Compare commits

..

No commits in common. "master" and "issue-47" have entirely different histories.

228 changed files with 6491 additions and 14456 deletions

View file

@ -30,17 +30,19 @@ MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=noreply-sw@ldc.fr
MAIL_FROM_NAME="${APP_NAME}"
MAIL_LIST_SERVICEWEB=digital@ldc.fr
MAIL_ADMIN=digital@ldc.fr
MAIL_LIST_SERVICEWEB=equipe.web@ldc.fr
MAIL_ADMIN=equipe.web@ldc.fr
# Sentry
SENTRY_LARAVEL_DSN=
# UWIL ADMIN SECURE
# - LIST SEPARATED WITH ',' (Required)
UWIL_ADMIN_SECURE_CIDR_LIST=""
# ADMIN SECURE
# - API ENDPOINT (Optional)
LDC_WEB_API_ENDPOINT=
# - API KEY
LDC_WEB_API_KEY=
# - LIST SEPARATED WITH ',' (Optional)
UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST=
LDC_WEB_CIDR_MANUAL_ALLOWED_LIST=
# GITLAB API
GITLAB_URL=

5
.gitignore vendored
View file

@ -21,8 +21,3 @@ Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
storage/samlidp/*
.npmrc
.zshrc
.npm/
.composer/

View file

@ -7,7 +7,7 @@ include:
file: 'security/security.yml'
- project: 'ldc-web/shared-ci'
ref: 'main'
file: "build/docker-multistage.yml"
file: 'build/build.yml'
- project: 'ldc-web/shared-ci'
ref: 'main'
file: 'test/test.yml'

View file

@ -1,43 +1,5 @@
FROM composer:2.9.2 AS vendor
ARG GITLAB_USER
ARG GITLAB_PASSWORD
COPY database/ database/
COPY composer.json composer.json
COPY composer.lock composer.lock
RUN composer config gitlab-token.lab.s-web.fr ${GITLAB_USER} ${GITLAB_PASSWORD}
RUN composer install \
--ignore-platform-reqs \
--no-interaction \
--no-plugins \
--no-scripts \
--no-dev \
--prefer-dist
FROM node:20.18 AS frontend
ARG GITLAB_PASSWORD
RUN mkdir -p /app/public/build
COPY package*.json tsconfig.json vite.config.ts .eslintrc.json .stylelintrc.json /app/
COPY resources/ /app/resources/
WORKDIR /app
RUN npm config set @uwil:registry https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/
RUN npm config set -- '//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken' "${GITLAB_PASSWORD}"
RUN npm ci --no-audit --no-fund --loglevel=error && npm run prod
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/uwil-production-8.5:${PHP_8_5_TAG}
ARG USER=www-data
COPY --chown=${USER}:${USER} . /app/srcs
COPY --from=vendor --chown=${USER}:${USER} /app/vendor/ /app/srcs/vendor/
COPY --from=frontend --chown=${USER}:${USER} /app/public/build/ /app/srcs/public/build/
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/ldcweb-production-8.2:latest
COPY --chown=www-data:www-data . /app/srcs
USER root
RUN chmod -R 755 /app/srcs/storage
USER ${USER}
USER www-data

View file

@ -1,17 +1,3 @@
# UWIL Support
PROD : support.uwil.fr
RE7 : support-web.re7-s-web.fr
## Gitlab Intégration
Le token de groupe pour l'intégration est valable 11 mois
Il faut le renouveler ici : https://lab.s-web.fr/groups/ldc-web/projets/-/settings/access_tokens et le nommer SW Intégration
Role : Guest
Scope : api
## TODO
# TODO
- App\Models\Message:booted(): investigate why this make errors on prod,

File diff suppressed because it is too large Load diff

View file

@ -117,7 +117,7 @@ class CreateUser extends Command
{
while (is_null($this->email)) {
try {
$tmp = $this->ask('Type the wanted user email', 'digital@ldc.fr');
$tmp = $this->ask('Type the wanted user email', 'equipe.web@ldc.fr');
$validator = Validator::make(['email' => $tmp], [
'email' => 'required|unique:users,email|email:rfc,dns'
]);

View file

@ -1,38 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Jobs\ExportTicketsState as JobsExportTicketsState;
use Illuminate\Console\Command;
class ExportTicketsState extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'tickets:export-state';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Export the state of tickets to a CSV file';
/**
* Execute the console command.
*
* @return integer
*/
public function handle()
{
$this->info('Running the Job, this could take some time.');
$job = new JobsExportTicketsState();
$job->handle();
$this->info('Job completed');
return 0;
}
}

View file

@ -19,7 +19,7 @@ class TestMail extends Command
*
* @var string
*/
protected $description = 'Send test email to digital@ldc.fr';
protected $description = 'Send test email to equipe.web@ldc.fr';
/**
* Execute the console command.
@ -29,7 +29,7 @@ class TestMail extends Command
public function handle()
{
Mail::raw(sprintf('UWIL mail-test from %s %s', \config('app.name'), \config('app.url')), function ($msg) {
$msg->to('digital@ldc.fr')->subject('Test Email');
$msg->to('equipe.web@ldc.fr')->subject('Test Email');
});
return 0;
}

View file

@ -1,334 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Enums\Tickets\TicketStatusEnum;
use App\Models\GitlabIssues;
use App\Models\GitlabLabels;
use App\Models\GitlabMilestones;
use App\Models\GitlabProjects;
use App\Models\Ticket;
use Carbon\Carbon;
use GrahamCampbell\GitLab\Facades\GitLab;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class UpdateGitlabDatas extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:update-gitlab-datas {--is-cron}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fetch some Gitlab datas';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$isCron = $this->option('is-cron');
$groupId = 23;
$this->updateProjectsByGroup($groupId);
$this->updateLabelsByGroup($groupId);
$projects = GitlabProjects::all();
foreach ($projects as $project) {
$this->updateMilestonesByProject($project);
$this->updateIssuesByProject($project, $isCron);
}
$this->syncTicketStatus();
}
/**
* updateProjectsByGroup function
*
* @param integer $groupId
* @return void
*/
private function updateProjectsByGroup(int $groupId): void
{
$allProjects = [];
$page = 1;
do {
$projects = GitLab::groups()->projects($groupId, [
'order_by' => 'created_at',
'sort' => 'asc',
'per_page' => 100,
'page' => $page,
'include_subgroups' => true
]);
$allProjects = array_merge($allProjects, $projects);
$page++;
} while (!empty($projects));
$this->info(sprintf(
"Fetched %d projects in group %d.",
count($allProjects),
$groupId
));
foreach ($allProjects as $project) {
GitlabProjects::upsert(
[
'name' => $project['name'],
'url' => $project['web_url'],
'is_archived' => $project['archived'],
'gitlab_id' => $project['id']
],
['gitlab_id']
);
}
$this->info("Gitlab Projects upsert complete.");
}
/**
* updateLabelsByGroup function
*
* @param integer $groupId
* @return void
*/
private function updateLabelsByGroup(int $groupId): void
{
$allLabels = [];
$page = 1;
do {
$labels = GitLab::groups()->labels($groupId, [
'per_page' => 100,
'page' => $page,
]);
$allLabels = array_merge($allLabels, $labels);
$page++;
} while (!empty($labels));
$this->info(sprintf(
"Fetched %d labels in group %d.",
count($allLabels),
$groupId
));
foreach ($allLabels as $project) {
GitlabLabels::upsert(
[
'name' => $project['name'],
'gitlab_id' => $project['id']
],
['gitlab_id']
);
}
$this->info("Gitlab labels upsert complete.");
}
/**
* updateIssuesByProject function
*
* @param GitlabProjects $project
* @param boolean $isCron
* @return void
*/
private function updateIssuesByProject(GitlabProjects $project, bool $isCron = false): void
{
$projectId = $project->gitlab_id;
$allIssues = [];
$page = 1;
do {
$params = [
'order_by' => 'created_at',
'sort' => 'asc',
'per_page' => 100,
'page' => $page,
'state' => 'all',
];
if ($isCron) {
$params = array_merge($params, ['updated_after' => now()->subDay()]);
}
$issues = GitLab::projects()->issues($projectId, $params);
$allIssues = array_merge($allIssues, $issues);
$page++;
} while (!empty($issues));
$this->info(sprintf(
"Fetched %d issues in project %d.",
count($allIssues),
$projectId
));
foreach ($allIssues as $issue) {
GitlabIssues::upsert(
[
'title' => $issue['title'],
'url' => $issue['web_url'],
'estimate_time' => $issue['time_stats']['time_estimate'] ?? 0,
'spent_time' => $issue['time_stats']['total_time_spent'] ?? 0,
'is_closed' => $issue['state'] === 'closed',
'gitlab_id' => $issue['id'],
'gitlab_iid' => $issue['iid'],
'milestone_iid' => $issue['milestone']['iid'] ?? null,
'gitlab_project_id' => $projectId,
'g_created_at' => Carbon::parse($issue['created_at'])->format('Y-m-d'),
'g_updated_at' => Carbon::parse($issue['updated_at'])->format('Y-m-d'),
'g_closed_at' => !is_null($issue['closed_at'])
? Carbon::parse($issue['closed_at'])->format('Y-m-d')
: null,
'g_due_date' => !is_null($issue['due_date'])
? Carbon::parse($issue['due_date'])->format('Y-m-d')
: null
],
['gitlab_id']
);
$issueModel = GitlabIssues::where('gitlab_id', $issue['id'])->firstOrFail();
$labelIds = [];
foreach ($issue['labels'] as $labelName) {
$label = Cache::remember("gitlab_label_{$labelName}", 300, function () use ($labelName) {
return GitlabLabels::where('name', $labelName)->first();
});
if ($label) {
$labelIds[] = $label;
}
}
$issueModel->gitlabLabels()->sync($labelIds);
}//end foreach
$this->info("Gitlab issues upsert complete.");
}
/**
* updateMilestonesByProject function
*
* @param GitlabProjects $project
* @return void
*/
private function updateMilestonesByProject(GitlabProjects $project): void
{
$projectId = $project->gitlab_id;
$allMilestones = [];
$page = 1;
do {
$milestones = [];
try {
$milestones = GitLab::milestones()->all($projectId, [
'per_page' => 100,
'page' => $page
]);
} catch (\Exception $e) {
if ($e->getCode() === 404) {
if ($project->delete()) {
Log::warning(sprintf(
"Project not found (%s), it has been deleted.",
$projectId
));
} else {
Log::warning(sprintf(
"Project not found (%s).",
$projectId
));
}
} else {
Log::error(sprintf(
'API Error for project %s, Message: %d',
$projectId,
$e->getMessage()
));
report($e);
}
}
$allMilestones = array_merge($allMilestones, $milestones);
$page++;
} while (!empty($milestones));
$this->info(sprintf(
"Fetched %d milestones in project %d.",
count($allMilestones),
$projectId
));
foreach ($allMilestones as $milestone) {
GitlabMilestones::upsert(
[
'title' => $milestone['title'],
'url' => $milestone['web_url'],
'is_closed' => $milestone['state'] === 'closed',
'gitlab_id' => $milestone['id'],
'gitlab_iid' => $milestone['iid'],
'gitlab_project_id' => $projectId,
'g_created_at' => Carbon::parse($milestone['created_at'])->format('Y-m-d'),
'g_updated_at' => Carbon::parse($milestone['updated_at'])->format('Y-m-d'),
'g_start_date' => !is_null($milestone['start_date'])
? Carbon::parse($milestone['start_date'])->format('Y-m-d')
: null,
'g_due_date' => !is_null($milestone['due_date'])
? Carbon::parse($milestone['due_date'])->format('Y-m-d')
: null
],
['gitlab_id']
);
}//end foreach
$this->info("Gitlab milestones upsert complete.");
}
/**
* Synchronizes ticket statuses based on GitLab issue labels.
*
* @return void
*/
private function syncTicketStatus(): void
{
$tickets = Ticket::whereNotNull('gitlab_issue')->whereNot('status', TicketStatusEnum::archived)->get();
foreach ($tickets as $ticket) {
$gitlabIssue = GitlabIssues::where('url', $ticket->gitlab_issue)->with('gitlabLabels')->first();
$gitlabIssueLabels = $gitlabIssue ? $gitlabIssue->gitlabLabels->pluck('name') : [];
if ($gitlabIssueLabels) {
match (true) {
$gitlabIssueLabels->contains('g-wip') => $this->updateTicketStatus($ticket, TicketStatusEnum::inDev),
$gitlabIssueLabels->contains('g-production') => $this->updateTicketStatus($ticket, TicketStatusEnum::production),
default => null,
};
}
}
}
/**
* Updates the ticket status only if it has changed.
*
* @param Ticket $ticket The ticket to update.
* @param TicketStatusEnum $status The new status to apply.
*
* @return void
*/
private function updateTicketStatus(Ticket $ticket, TicketStatusEnum $status): void
{
if ($ticket->status !== $status && $ticket->status !== TicketStatusEnum::waiting) {
$ticket->status = $status;
$ticket->saveOrFail();
}
}
}

View file

@ -3,7 +3,6 @@
namespace App\Console;
use App\Jobs\AutoCloseOldTickets;
use App\Jobs\ExportTicketsState;
use App\Jobs\WarnBeforeAutoCloseTicket;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@ -25,10 +24,8 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
$schedule->job(ExportTicketsState::class)->weekly()->fridays()->at('14:00');
$schedule->job(AutoCloseOldTickets::class)->dailyAt('08:00');
$schedule->job(WarnBeforeAutoCloseTicket::class)->dailyAt('08:05');
$schedule->command('app:update-gitlab-datas --is-cron')->hourly()->between('7:00', '19:00');
}
/**

View file

@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Dto;
final readonly class BrevoWebHookDto
{
public function __construct(
public readonly string $plainTextContent,
public readonly ?string $brand
) {}
}

View file

@ -1,49 +0,0 @@
<?php
namespace App\Enums\Credit;
use App\Traits\Enums\BaseEnum;
enum EstimationTimeEnum: int
{
use BaseEnum;
case halfHour = 0.0625;
case hour = 0.125;
case hourAndHalf = 0.1875;
case twoHours = 0.25;
case twoHoursAndHalf = 0.3125;
case threeHours = 0.375;
case threeHoursAndHalf = 0.4375;
case fourHours = 0.5;
case fourHoursAndHalf = 0.5625;
case fiveHours = 0.625;
case fiveHoursAndHalf = 0.6875;
case sixHours = 0.75;
case sixHoursAndHalf = 0.8125;
case sevenHours = 0.875;
case sevenHoursAndHalf = 0.9375;
case eightHours = 1;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::halfHour->name => "Une demi-heure",
self::hour->name => "Une heure",
self::hourAndHalf->name => "Une heure et demie",
self::twoHours->name => "Deux heures",
self::twoHoursAndHalf->name => "Deux heures et demie",
self::threeHours->name => "Trois heures",
self::twoHoursAndHalf->name => "Trois heures et demie",
self::fourHours->name => "Quatre heures",
self::fourHoursAndHalf->name => "Quatre heures et demie",
self::fiveHours->name => "Cinq heures",
self::fiveHoursAndHalf->name => "Cinq heures et demie",
self::sixHours->name => "Six heures",
self::sixHoursAndHalf->name => "Six heures et demie",
self::sevenHours->name => "Sept heures",
self::sevenHoursAndHalf->name => "Sept heures et demi",
self::eightHours->name => "Huit heures",
];
}

View file

@ -8,12 +8,10 @@ enum InvoiceStatusEnum: int
{
use BaseEnum;
case opened = 1;
case refused = 2;
case canceled = 3;
case archived = 4;
case awaiting_signature = 5;
case awaiting_invoicing = 6;
case opened = 1;
case refused = 2;
case canceled = 3;
case archived = 4;
/**
* Optionnal labels definition.
@ -23,7 +21,5 @@ enum InvoiceStatusEnum: int
self::refused->name => "Refusé (par le client ou aucune réponse de sa part)",
self::canceled->name => "Annulé (supprimé)",
self::archived->name => "Archivé (validé et envoyé à la comptabilité)",
self::awaiting_signature->name => "En attente de signature",
self::awaiting_invoicing->name => "En attente de facturation",
];
}

View file

@ -27,7 +27,7 @@ enum TaskTypeEnum: int
* Custom added prices definition
*/
private const PRICES = [
self::web->name => 600,
self::web->name => 500,
self::seo->name => 690,
// * Linked to packages table
self::package->name => 0,

View file

@ -1,70 +0,0 @@
<?php
namespace App\Enums\Tickets;
use App\Traits\Enums\BaseEnum;
enum TicketFileTypeEnum: int
{
use BaseEnum;
case png = '.png';
case jpeg = '.jpeg';
case jpg = '.jpg';
case pdf = '.pdf';
case pptx = '.pptx';
case docx = '.docx';
case xlsx = '.xlsx';
case csv = '.csv';
case svg = '.svg';
/**
* Optionnal labels definition
*/
private const LABELS = [
self::png->name => "Fichier png",
self::jpeg->name => "Fichier jpeg",
self::jpg->name => "Fichier jpg",
self::pdf->name => "Fichier pdf",
self::pptx->name => "Fichier pptx",
self::docx->name => "Fichier docx",
self::xlsx->name => "Fichier xlsx",
self::csv->name => "Fichier csv",
self::svg->name => "Fichier svg",
];
/**
* Custom added mimes type definition
*/
private const MIMETYPE = [
self::png->name => 'image/png',
self::jpeg->name => 'image/jpeg',
self::jpg->name => 'image/jpeg',
self::pdf->name => 'application/pdf',
self::pptx->name => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
self::docx->name => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
self::xlsx->name => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
self::csv->name => 'text/csv',
self::svg->name => 'image/svg+xml',
];
/**
* Get Class
*
* @return string
*/
public function mimetypes(): string
{
return self::MIMETYPE[$this->name];
}
/**
* Get enum classes
*
* @return array<string>
*/
public static function toMimetypes(): array
{
return self::MIMETYPE;
}
}

View file

@ -14,25 +14,21 @@ enum TicketStatusEnum: int
case waiting = 4;
case waitingValidation = 5;
case recette = 6;
case recetteValidated = 7;
case production = 8;
case archived = 9;
case inDev = 10;
case production = 7;
case archived = 8;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::new->name => "Nouveau",
self::estimateTodo->name => "Étude chef(s) de projet",
self::takenInAccount->name => "À planifier",
self::estimateTodo->name => "Devis à faire",
self::takenInAccount->name => "Pris en compte",
self::waiting->name => "En attente de réponse",
self::waitingValidation->name => "En attente de validation",
self::recette->name => "En recettage",
self::recetteValidated->name => "Recettage validé",
self::production->name => "En production",
self::archived->name => "Archivé",
self::inDev->name => "En cours de développement",
];
/**
@ -45,10 +41,8 @@ enum TicketStatusEnum: int
self::waiting->name => 'text-white bg-danger',
self::waitingValidation->name => 'text-warning bg-danger',
self::recette->name => 'text-primary',
self::recetteValidated->name => 'text-success',
self::production->name => 'text-success',
self::archived->name => 'text-secondary',
self::inDev->name => 'text-discovery',
];
/**

View file

@ -8,14 +8,12 @@ enum TicketTypeEnum: int
{
use BaseEnum;
case bug = 1;
case evolution = 2;
case br = 3;
case newsletter = 4;
case qrcode = 5;
case advice = 6;
case complaints = 7;
case mailing = 8;
case bug = 0;
case evolution = 1;
case br = 2;
case newsletter = 3;
case qrcode = 4;
case advice = 5;
/**
* Optionnal labels definition
@ -27,8 +25,6 @@ enum TicketTypeEnum: int
self::newsletter->name => "Demande newsletter",
self::qrcode->name => "Demande de QR Code",
self::advice->name => "Conseil - Accompagnement",
self::complaints->name => "Réclamations consommateurs",
self::mailing->name => "Demande mail en masse",
];
/**
@ -41,8 +37,6 @@ enum TicketTypeEnum: int
self::newsletter->name => "bg-warning",
self::qrcode->name => "bg-secondary",
self::advice->name => "bg-info",
self::complaints->name => "bg-dark",
self::mailing->name => "bg-discovery",
];
/**

View file

@ -0,0 +1,20 @@
<?php
namespace App\Enums\Users;
use App\Traits\Enums\BaseEnum;
enum GitlabUsersEnum: int
{
use BaseEnum;
// Each user is associated to his ID on gitlab.
case olivier = 11;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::olivier->name => "Olivier LEGAT",
];
}

View file

@ -1,30 +0,0 @@
<?php
namespace App\Events;
use Domain\Models\ValueObjects\Email;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
readonly class BrevoWebhookCallReceived
{
use Dispatchable;
public function __construct(
public Email $from,
public Email $to,
public string $htmlRawContent
) {}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('default'),
];
}
}

View file

@ -1,34 +0,0 @@
<?php
namespace App\Events;
use App\Models\Site;
use App\Models\Ticket;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class TicketCreated implements ShouldQueue
{
use Dispatchable;
use SerializesModels;
public function __construct(
public readonly Site $siteReceivingTicket,
public readonly Ticket $ticket,
public readonly string $byUserName
) {}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('default'),
];
}
}

View file

@ -1,58 +0,0 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Events\BrevoWebhookCallReceived;
use App\Http\Requests\Bo\BrevoWebhookRequest;
use Domain\Models\ValueObjects\Email;
use Psr\Log\LoggerInterface;
use Illuminate\Routing\Controller as BaseController;
use \Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
class BrevoWebhookController extends BaseController
{
public function __construct(
private readonly LoggerInterface $logger,
private readonly Dispatcher $dispatcher
) {}
public function webhookCallReceived(BrevoWebhookRequest $request): JsonResponse
{
$fromEmail = $request->getFromAddress();
$toEmail = $request->getToAddress();
$formContent = $request->getRawHtmlBody();
if (!$this->isFromLdc($fromEmail)) {
$this->logger->warning(
'Received a brevo webhook call from a non ldc email.',
[
'from' => $fromEmail,
'to' => $toEmail,
'content' => base64_encode($formContent)
]
);
return response()->json(
['ok' => false],
Response::HTTP_BAD_REQUEST
);
}
$this->dispatcher->dispatch(
new BrevoWebhookCallReceived(
from: $fromEmail,
to: $toEmail,
htmlRawContent: $formContent,
)
);
return response()->json(['ok' => true]);
}
private function isFromLdc(Email $email): bool
{
return substr(strrchr($email->value, "@"), 1) === 'ldc.fr';
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Models\Dutyhour;
use Illuminate\Contracts\View\View;
class DashboardController extends Controller
{
/**
* Show Home page
*
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function index(): View
{
/**
* Get only duty hours for today or later.
*
* @var \Illuminate\Database\Eloquent\Builder $dutyhour
*/
$dutyhourModels = Dutyhour::where('date', '>', now()->modify('-1 day'))
->orderBy('date', 'asc')
->limit(10)
->get();
return view('home', compact('dutyhourModels'));
}
}

View file

@ -0,0 +1,157 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Dutyhours\StoreDutyhourRequest;
use App\Http\Requests\Bo\Dutyhours\UpdateDutyhourRequest;
use App\Models\Dutyhour;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class DutyhourController extends Controller
{
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $dutyhourModels */
$dutyhourModels = Dutyhour::query();
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$dutyhourModels,
$search,
null,
'date',
'person',
'place'
);
}
$searchFields = trans('Date, personne, lieu');
/** Sort columns with a query */
$this->sortQuery($dutyhourModels);
/** Custom pagination */
$dutyhourModels = $this->customPaginate($dutyhourModels, $request->pagination);
return view('bo.pages.dutyhours.index', compact('dutyhourModels', 'search', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Dutyhour $dutyhour
* @return \Illuminate\Contracts\View\View
*/
public function create(Dutyhour $dutyhour): \Illuminate\Contracts\View\View
{
return view('bo.pages.dutyhours.create', [
'dutyhourModel' => $dutyhour
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Dutyhours\StoreDutyhourRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreDutyhourRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$dutyhour = new Dutyhour();
$dutyhour->fill($request->validated());
if ($dutyhour->saveOrFail()) {
return redirect()
->route('bo.dutyhours.edit', $dutyhour)
->with('success', trans(
'crud.messages.has_been_created',
['model' => Str::singular(trans('models.classes.dutyhours'))]
));
}
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_created',
['model' => Str::singular(trans('models.classes.dutyhours'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Dutyhour $dutyhour
* @return \Illuminate\Contracts\View\View
*/
public function edit(Dutyhour $dutyhour): \Illuminate\Contracts\View\View
{
return view('bo.pages.dutyhours.edit', [
'dutyhourModel' => $dutyhour
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Dutyhours\UpdateDutyhourRequest $request
* @param \App\Models\Dutyhour $dutyhour
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateDutyhourRequest $request, Dutyhour $dutyhour): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $dutyhour) {
$dutyhour->fill($request->validated());
if ($dutyhour->saveOrFail()) {
return redirect()
->route('bo.dutyhours.edit', $dutyhour)
->with('success', trans(
'crud.messages.has_been_updated',
['model' => Str::singular(trans('models.classes.dutyhours'))]
));
} //end if
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_updated',
['model' => Str::singular(trans('models.classes.dutyhours'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Dutyhour $dutyhour
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Dutyhour $dutyhour): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($dutyhour) {
if ($dutyhour->deleteOrFail()) {
return redirect()
->route('bo.dutyhours.index')
->with('success', trans(
'crud.messages.has_been_deleted',
['model' => Str::singular(trans('models.classes.dutyhours'))]
));
}
return redirect()
->back()
->with('error', trans(
':crud.messages.cannot_be_deleted',
['model' => Str::singular(trans('models.classes.dutyhours'))]
));
});
}
}

View file

@ -25,27 +25,12 @@ class InvoiceController extends Controller
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
// If no sorting is requested, apply the default orderByDesc.
/** @var \Illuminate\Database\Eloquent\Builder $invoiceModels */
$invoiceModels = (!request()->query('sort_col') && !request()->query('sort_way'))
? Invoice::query()->orderByDesc('created_at')
: Invoice::query();
$status = intval($request->status);
switch ($status) {
case InvoiceStatusEnum::awaiting_signature->value:
$invoiceModels->whereNotNull('sent_client_date')->whereNull('signed_date');
break;
case InvoiceStatusEnum::awaiting_invoicing->value:
$invoiceModels->whereNotNull('sent_client_date')->whereNotNull('signed_date')->whereNull('sent_accounting_date');
break;
default:
if ($status !== 0) {
$invoiceModels->where('status', $status);
}
break;
}
$invoiceModels = Invoice::query();
$status = intval($request->status);
$invoiceModels = $invoiceModels->when($status, function ($query) use ($status) {
$query->where('status', $status);
});
/** @var string $search Search field */
$search = $request->search;
@ -56,11 +41,9 @@ class InvoiceController extends Controller
null,
'firstname',
'lastname',
'ref',
'site_id',
);
}
$searchFields = trans('Nom, Prénom, Référence, Site');
$searchFields = trans('Nom, Prénom');
/** Sort columns with a query */
$this->sortQuery($invoiceModels);
@ -90,7 +73,7 @@ class InvoiceController extends Controller
*/
public function create(Invoice $invoice): \Illuminate\Contracts\View\View
{
$packageModels = Package::where('published', true)->get();
$packageModels = Package::get();
$taskRowModel = Invoice::getTaskStructure();
$siteModels = Site::orderBy('name', 'ASC')->get();
$companyModels = Company::get();
@ -197,10 +180,7 @@ class InvoiceController extends Controller
return $invoice->generateInvoice()
->stream(Str::slug(trans('Devis :appname :date - ref :ref', [
'appname' => config('app.name'),
'date' =>
$invoice->created_at !== $invoice->updated_at ?
$invoice->updated_at->isoFormat('YYYY-MM-DD') :
$invoice->created_at->isoFormat('YYYY-MM-DD'),
'date' => $invoice->created_at->isoFormat('YYYY-MM-DD'),
'ref' => $invoice->ref,
])) . '.pdf');
}
@ -220,7 +200,7 @@ class InvoiceController extends Controller
{
$model = $request->targetModel;
return DB::transaction(function () use ($model) {
$models = $model::query()->orderBy('name', 'ASC')->paginate(12);
$models = $model::orderBy('name', 'ASC')->paginate(12);
return \response()->json($models ?? 500);
});
}

View file

@ -23,15 +23,6 @@ class PackageController extends Controller
/** @var \Illuminate\Database\Eloquent\Builder $packageModels */
$packageModels = Package::query();
$status = $request->status;
if ($status === null) {
$packageModels->where('published', true);
} elseif ($status === '0') {
$packageModels->where('published', false);
} elseif ($status === '1') {
$packageModels->where('published', true);
}
/** @var string $search Search field */
$search = $request->search;
if ($search) {
@ -51,7 +42,7 @@ class PackageController extends Controller
/** Custom pagination */
$packageModels = $this->customPaginate($packageModels, $request->pagination);
return view('bo.pages.packages.index', compact('packageModels', 'search', 'searchFields', 'status'));
return view('bo.pages.packages.index', compact('packageModels', 'search', 'searchFields'));
}
/**
@ -162,16 +153,4 @@ class PackageController extends Controller
));
});
}
/**
* Change model published.
*
* @param \App\Models\Package $package
* @return \Illuminate\Http\RedirectResponse
*/
public function changePublished(Package $package)
{
$package->update(['published' => !$package->getOriginal('published')]);
return redirect()->back()->with('success', trans(__('crud.messages.publish_status_saved')));
}
}

View file

@ -0,0 +1,193 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Projects\StoreProjectRequest;
use App\Http\Requests\Bo\Projects\UpdateProjectRequest;
use App\Lib\Helpers\ToolboxHelper;
use App\Models\Project;
use App\Models\Site;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class ProjectController extends Controller
{
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $projectModels */
$projectModels = Project::query();
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$projectModels,
$search,
null,
'name',
);
}
$searchFields = trans('Nom');
/** Sort columns with a query */
$this->sortQuery($projectModels);
/** Custom pagination */
$projectModels = $this->customPaginate($projectModels, $request->pagination);
return view('bo.pages.projects.index', compact('projectModels', 'search', 'searchFields'));
}
/**
* Crud show view
*
* @param \App\Models\Project $project
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
*/
public function show(Project $project)
{
return view('bo.pages.projects.show', [
'projectModel' => $project
]);
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Project $project
* @return \Illuminate\Contracts\View\View
*/
public function create(Project $project): \Illuminate\Contracts\View\View
{
$siteModels = Site::orderBy('name', 'ASC')->get();
return view('bo.pages.projects.create', [
'projectModel' => $project,
'siteModels' => $siteModels
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Projects\StoreProjectRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreProjectRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$project = new Project();
$project->fill($request->validated());
if ($project->saveOrFail()) {
return redirect()
->route('bo.projects.edit', $project)
->with('success', trans(
'crud.messages.has_been_created',
['model' => Str::singular(trans('models.classes.projects'))]
));
}
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_created',
['model' => Str::singular(trans('models.classes.projects'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Project $project
* @return \Illuminate\Contracts\View\View
*/
public function edit(Project $project): \Illuminate\Contracts\View\View
{
$siteModels = Site::orderBy('name', 'ASC')->get();
return view('bo.pages.projects.edit', [
'projectModel' => $project,
'siteModels' => $siteModels
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Projects\UpdateProjectRequest $request
* @param \App\Models\Project $project
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateProjectRequest $request, Project $project): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $project) {
$project->fill($request->validated());
if ($project->saveOrFail()) {
return redirect()
->route('bo.projects.edit', $project)
->with('success', trans(
'crud.messages.has_been_updated',
['model' => Str::singular(trans('models.classes.projects'))]
));
} //end if
return redirect()
->back()
->with('error', trans(
'crud.messages.cannot_be_updated',
['model' => Str::singular(trans('models.classes.projects'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Project $project
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Project $project): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($project) {
if ($project->deleteOrFail()) {
return redirect()
->route('bo.projects.index')
->with('success', trans(
'crud.messages.has_been_deleted',
['model' => Str::singular(trans('models.classes.projects'))]
));
}
return redirect()
->back()
->with('error', trans(
':crud.messages.cannot_be_deleted',
['model' => Str::singular(trans('models.classes.projects'))]
));
});
}
/**
* Get a paginate resource.
*
* @param \Illuminate\Http\Request $request
* @return Illuminate\Http\JsonResponse
*/
public function jsonPaginate(Request $request): \Illuminate\Http\JsonResponse
{
return DB::transaction(function () use ($request) {
$model = $request->targetModel;
$search = $request->search;
$models = $model::where("name", "LIKE", "%{$search}%")->orderBy("name", "ASC");
return ToolboxHelper::jsonPaginate($this->customPaginate($models, $request->pagination));
});
}
}

View file

@ -9,7 +9,6 @@ use App\Lib\Helpers\ToolboxHelper;
use App\Models\Site;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
@ -24,15 +23,15 @@ class SiteController extends Controller
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \App\Models\User $user */
$user = Auth::user();
$user = \auth()->user();
if ($user->isAdmin()) {
/** @var \Illuminate\Database\Eloquent\Builder $siteModels */
$siteModels = Site::query()->with(['invoices', 'tickets']);
$siteModels = Site::query();
} else {
/** @var \Illuminate\Database\Eloquent\Builder $siteModels */
$siteModels = Site::whereHas('users', function (\Illuminate\Database\Eloquent\Builder $query) use ($user) {
$query->where('id', $user->id)->with(['invoices', 'tickets']);
$query->where('id', $user->id);
});
}

View file

@ -2,48 +2,41 @@
namespace App\Http\Controllers\Bo;
use App\Models\Site;
use App\Models\User;
use App\Models\Ticket;
use App\Mail\NewTicket;
use App\Models\Message;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use App\Jobs\UpdateIssueLabels;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Support\Facades\DB;
use App\Enums\Tickets\TicketStatusEnum;
use App\Enums\Tickets\TicketTypeEnum;
use App\Enums\Users\GitlabUsersEnum;
use App\Events\NotifySubscriptions;
use App\Events\TicketStatusChanged;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\LinkTicket;
use Illuminate\Support\Facades\Mail;
use App\Enums\Tickets\TicketTypeEnum;
use App\Http\Requests\Bo\AssignTicketToUserRequest;
use App\Http\Requests\Bo\ChangeTicketStatus;
use App\Http\Requests\Bo\CreateMessage;
use App\Http\Requests\Bo\SortTickets;
use App\Http\Requests\Bo\StoreTicket;
use App\Http\Requests\Bo\UpdateMessage;
use App\Http\Requests\Bo\UpdateTicket;
use App\Lib\Helpers\FileStorageHelper;
use App\Enums\Tickets\TicketStatusEnum;
use App\Events\TicketCreated;
use App\Http\Requests\Bo\CreateMessage;
use App\Http\Requests\Bo\UpdateMessage;
use Illuminate\Support\Facades\Session;
use App\Lib\Helpers\ToolboxHelper;
use App\Mail\NewTicket;
use App\Models\Message;
use App\Models\Site;
use App\Models\Ticket;
use App\Models\User;
use GrahamCampbell\GitLab\Facades\GitLab;
use Illuminate\Database\Eloquent\Builder;
use App\Http\Requests\Bo\ChangeTicketStatus;
use App\Http\Requests\Bo\UpdateChangeOrderRequest;
use App\Http\Requests\Bo\AssignTicketToUserRequest;
use App\Http\Requests\Bo\UpdateTicketPriorityRequest;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
class TicketController extends Controller
{
/**
* TicketController
*/
public function __construct(
public readonly Dispatcher $dispatcher
) {
public function __construct()
{
parent::__construct();
$this->authorizeResource(Ticket::class, 'ticket');
}
@ -81,6 +74,7 @@ class TicketController extends Controller
}));
} //end if
/** @var \App\Models\User */
$user = auth()->user();
$tickets = Ticket::with(['site', 'labels']);
@ -101,30 +95,15 @@ class TicketController extends Controller
TicketStatusEnum::archived
);
if (!$user->isAdmin() && $user->sites->count() === 1) {
$request->merge([
'pr' => $user->sites->first()->id,
]);
}
$tickets = $this->filterTickets($tickets, $request);
$tickets = $this->searchTicket($tickets, $request);
/** Sort columns with a query */
$this->sortQuery($tickets);
/** Custom pagination */
$tickets = $this->customPaginate($tickets, $request->pagination);
$sites = Site::select('id', 'name')->orderBy('name')->get();
$status = Arr::sort(TicketStatusEnum::cases(), function (TicketStatusEnum $value) {
return $value->label();
});
$types = Arr::sort(TicketTypeEnum::cases(), function (TicketTypeEnum $value) {
return $value->label();
});
$status = TicketStatusEnum::cases();
return view('bo.pages.tickets.index', compact('tickets', 'sites', 'status', 'types'));
return view('bo.pages.tickets.index', compact('tickets', 'sites', 'status'));
}
/**
@ -156,11 +135,7 @@ class TicketController extends Controller
$messages = $ticket->messages()->orderBy('created_at', 'DESC')->get();
$isSubscribed = (bool)$ticket->subscriptions()->where('user_id', $user->id)->count();
return view('bo.pages.tickets.show', [
'ticket' => $ticket,
'messages' => $messages,
'isSubscribed' => $isSubscribed
]);
return view('bo.pages.tickets.show', compact('ticket', 'messages', 'isSubscribed'));
}
/**
@ -203,20 +178,19 @@ class TicketController extends Controller
$ticket = new Ticket($request->validated());
/** @var array<\Illuminate\Http\UploadedFile> */
$fileTickets = $request->file_tickets ?? [];
if (count($fileTickets) > 0) {
foreach ($fileTickets as $key => $file) {
if ($key === 0) {
$ticket->file = FileStorageHelper::storeFile($ticket, $file);
} else {
$fileNumber = 'file' . $key + 1;
$ticket->$fileNumber = FileStorageHelper::storeFile($ticket, $file);
}
}
/** @var \Illuminate\Http\UploadedFile|null */
$fileTicket = $request->file_ticket;
/** @var \Illuminate\Http\UploadedFile|null */
$fileTicket2 = $request->file_ticket2;
if ($fileTicket) {
$ticket->file = FileStorageHelper::storeFile($ticket, $fileTicket);
}
if ($fileTicket2) {
$ticket->file2 = FileStorageHelper::storeFile($ticket, $fileTicket2);
}
$ticket->user()->associate($user);
$ticket->user()->associate(auth()->user());
$ticket->site()->associate($site);
$ticket->saveOrFail();
@ -226,11 +200,11 @@ class TicketController extends Controller
$user->subscriptions()->toggle($ticket->id);
$this->dispatcher->dispatch(new TicketCreated(
siteReceivingTicket: $site,
ticket: $ticket,
byUserName: $user->name
));
if (config('mail.MAIL_ADMIN')) {
Mail::to(config('mail.MAIL_ADMIN'))->send(new NewTicket($ticket));
} else {
throw new \ErrorException('Missing MAIL_ADMIN in .env');
}
return redirect()->route('bo.ticket.show', $ticket)
->with('success', trans('Ticket enregistré !'));
@ -278,18 +252,18 @@ class TicketController extends Controller
/** @var \App\Models\Site */
$site = Site::findOrFail($request->site_id);
/** @var null|array<\Illuminate\Http\UploadedFile> */
$fileTickets = $request->file_tickets;
if (isset($fileTickets) && count($fileTickets) > 0) {
foreach ($fileTickets as $key => $file) {
if ($key === 0) {
$ticket->file = FileStorageHelper::storeFile($ticket, $file);
} else {
$fileNumber = 'file' . $key + 1;
$ticket->$fileNumber = FileStorageHelper::storeFile($ticket, $file);
}
}
/** @var \Illuminate\Http\UploadedFile|null */
$fileTicket = $request->file_ticket;
/** @var \Illuminate\Http\UploadedFile|null */
$fileTicket2 = $request->file_ticket2;
if ($fileTicket) {
$ticket->file = FileStorageHelper::storeFile($ticket, $fileTicket);
}
if ($fileTicket2) {
$ticket->file2 = FileStorageHelper::storeFile($ticket, $fileTicket2);
}
$ticket->labels()->sync($request->labels);
@ -316,10 +290,6 @@ class TicketController extends Controller
*/
public function changeStatus(ChangeTicketStatus $request, Ticket $ticket)
{
if ($ticket->gitlab_issue) {
\dispatch(new UpdateIssueLabels($ticket, $request->status));
}
$ticket->fill($request->validated());
$ticket->saveOrFail();
@ -339,39 +309,32 @@ class TicketController extends Controller
try {
return DB::transaction(function () use ($request, $ticket) {
/** @var \Illuminate\Http\UploadedFile|null */
$filesMessage = $request->file_messages;
$message = new Message($request->validated());
$fileMessage = $request->file_message;
$message = new Message($request->validated());
$message->user()->associate(auth()->user());
$message->ticket()->associate($ticket);
/** @var \App\Models\User */
$user = auth()->user();
// Subscribe to ticket.
if ($request->subscribe) {
$user->subscriptions()->syncWithoutDetaching([$ticket->id]);
} else {
$user->subscriptions()->detach($ticket->id);
}
// Store files if any.
if (isset($filesMessage) and $filesMessage) {
foreach ($filesMessage as $key => $singleFile) {
if ($key === 0) {
$message->file = FileStorageHelper::storeFile($message, $singleFile);
$message->saveOrFail();
} else {
$fileNumber = 'file' . $key + 1;
$message->$fileNumber = FileStorageHelper::storeFile($message, $singleFile);
$message->saveOrFail();
}
if ($request->subscribe) {
if (!$user->subscriptions()->find($ticket->id)) {
$user->subscriptions()->attach($ticket->id);
}
} elseif ($user->subscriptions()->find($ticket->id)) {
$user->subscriptions()->detach($ticket->id);
}
$message->saveOrFail();
if ($fileMessage) {
$message->file = FileStorageHelper::storeFile($message, $fileMessage);
$message->saveOrFail();
}
return back()
->with('success', trans('Votre message à été ajouté'));
}, 5);
});
} catch (\ErrorException $e) {
report($e);
Session::flash('error', trans('Une erreur est survenue'));
@ -507,48 +470,6 @@ class TicketController extends Controller
return back()->with('info', trans('Le ticket n\'est plus assigné à personne'));
}
/**
* Link two tickets.
*
* @param LinkTicket $request
* @param Ticket $ticket
*
* @return \Illuminate\Http\RedirectResponse
*/
public function linkTicket(LinkTicket $request, Ticket $ticket)
{
$linkedTicket = Ticket::findOrFail($request->validated())->first();
if ($ticket->site_id !== $linkedTicket->site_id) {
return back()->with('error', trans('Le ticket que vous avez renseigné ne fait pas partie du même site.'));
}
if (!$ticket->linkedTickets()->where('linked_ticket_id', $linkedTicket->id)->exists()) {
$ticket->linkedTickets()->attach($linkedTicket->id);
$linkedTicket->linkedTickets()->attach($ticket->id);
return back()->with('success', trans('Les tickets ont été liés avec succès.'));
}
return back()->with('error', trans('Les tickets sont déjà liés ensemble.'));
}
/**
* Unlink two tickets.
*
* @param Ticket $ticket
* @param Ticket $linkedTicket
*
* @return \Illuminate\Http\RedirectResponse
*/
public function unlinkTicket(Ticket $ticket, Ticket $linkedTicket)
{
$ticket->linkedTickets()->detach($linkedTicket->id);
$linkedTicket->linkedTickets()->detach($ticket->id);
return back()->with('warning', trans('Le lien entre les tickets à été supprimé.'));
}
// * PRIVATES
/**
@ -566,10 +487,8 @@ class TicketController extends Controller
Session::remove('tickets.ass');
Session::remove('tickets.st');
Session::remove('tickets.pr');
Session::remove('tickets.ty');
Session::remove('tickets.sort_col');
Session::remove('tickets.sort_way');
Session::remove('tickets.or');
Session::remove('tickets.search');
}
if ($request->assr) {
@ -581,43 +500,21 @@ class TicketController extends Controller
if ($request->sts) {
Session::remove('tickets.st');
}
if ($request->tys) {
Session::remove('tickets.ty');
}
if ($request->sort_col) {
Session::put('tickets.sort_col', $request->sort_col);
}
if ($request->sort_way) {
Session::put('tickets.sort_way', $request->sort_way);
}
if ($request->ord) {
Session::remove('tickets.or');
}
if ($request->pr) {
Session::put('tickets.pr', $request->pr);
}
if ($request->st) {
Session::put('tickets.st', $request->st);
}
if ($request->ty) {
Session::put('tickets.ty', $request->ty);
}
if ($request->ass) {
Session::put('tickets.ass', $request->ass);
}
if (Session::has('tickets.or')) {
Session::remove('tickets.ass');
Session::remove('tickets.st');
Session::remove('tickets.ty');
Session::remove('tickets.sort_col');
Session::remove('tickets.sort_way');
Session::remove('tickets.search');
}
if ($request->or && Session::has('tickets.pr')) {
Session::put('tickets.or', $request->or);
}
Session::save();
if (Session::has('tickets.pr')) {
@ -634,24 +531,12 @@ class TicketController extends Controller
$tickets->where('status', Session::get('tickets.st'));
}
if (Session::has('tickets.ty')) {
$tickets->where('type', Session::get('tickets.ty'));
}
if (Session::has('tickets.or')) {
$tickets->orderBy('order', 'ASC');
}
if (
Session::has('tickets.sort_col') and
Session::has('tickets.sort_way') and
in_array(Session::get('tickets.sort_col'), [
'type',
'title',
'assigned_to',
'date_mep',
'created_at',
'created_by'
'id', 'site_id', 'created_at', 'created_by',
'type', 'status', 'assigned_to'
])
) {
$tickets->orderBy(
@ -661,7 +546,6 @@ class TicketController extends Controller
} else {
$tickets->orderBy('last_activity', 'DESC');
}
return $tickets;
}
@ -711,7 +595,7 @@ class TicketController extends Controller
});
} //end foreach
} //end if
}, 'text', 'title', 'file', 'file2', 'file3', 'file4', 'file5');
}, 'text', 'file', 'file2');
}
/**
@ -752,54 +636,33 @@ class TicketController extends Controller
*/
public function createGitlabIssue(Ticket $ticket)
{
$userGitlabId = auth()->user()->gitlab_id;
$gitlabRepoId = $ticket->site->gitlab_project_id;
if (GitLab::projects()->show($gitlabRepoId)['archived']) {
return back()->with('error', \trans("Ce projet GitLab est archivé et ne peut pas être utilisé."));
}
$description = \sprintf("%s<br>", $ticket->text);
$cLabel = match ($ticket->type) {
TicketTypeEnum::bug => 'c-bug,g-to-dispatch',
TicketTypeEnum::evolution,
TicketTypeEnum::mailing => 'c-feature',
TicketTypeEnum::newsletter => 'c-feature,g-to-estimate',
};
$assigneeId = match ($ticket->type) {
TicketTypeEnum::bug => $userGitlabId,
TicketTypeEnum::evolution,
TicketTypeEnum::mailing,
TicketTypeEnum::newsletter => $ticket->site->projectManager?->gitlab_id
$description = \sprintf("%s<br>", $ticket->text);
$cLabel = match ($ticket->type) {
TicketTypeEnum::bug => 'c-bug',
TicketTypeEnum::evolution => 'c-feature',
};
try {
foreach ($ticket->getFiles() as $key => $file) {
if (!\is_null($file)) {
$uploadedFile = GitLab::projects()->uploadFile($gitlabRepoId, $file);
$description .= $this->formatGitlabFiles($ticket, $uploadedFile, $key + 1);
}
if (!\is_null($ticket->file)) {
$uploadedFile = GitLab::projects()->uploadFile($gitlabRepoId, $ticket->file);
$description .= $this->formatGitlabFiles($ticket, $uploadedFile, 1);
}
$data = [
'title' => sprintf(
'ts-%s : %s',
$ticket->id,
$ticket->title
),
if (!\is_null($ticket->file2)) {
$uploadedFile2 = GitLab::projects()->uploadFile($gitlabRepoId, $ticket->file2);
$description .= $this->formatGitlabFiles($ticket, $uploadedFile2, 2);
}
$issue = GitLab::issues()->create($gitlabRepoId, [
'title' => 'ts-' . $ticket->id,
'description' => $description,
'labels' => \implode(",", [
'c-support',
$cLabel
]),
];
if ($assigneeId) {
$data['assignee_id'] = $assigneeId;
}
$issue = GitLab::issues()->create($gitlabRepoId, $data);
'assignee_id' => GitlabUsersEnum::olivier,
]);
} catch (\Exception $e) {
\report($e);
return back()->with('error', \trans("Une erreur est survenue"));
@ -827,7 +690,7 @@ class TicketController extends Controller
*/
private function formatGitlabFiles(Ticket $ticket, array $uploadedFile, int $fileNb)
{
if (Str::endsWith($ticket->file, ['jpg', 'png', 'jpeg', 'pptx', 'docx', 'xslx', 'csv', 'svg'])) {
if (Str::endsWith($ticket->file, ['jpg', 'png', 'jpeg'])) {
$file = \sprintf(
"![Fichier joint %d](%s)",
$fileNb,
@ -842,92 +705,4 @@ class TicketController extends Controller
}
return \sprintf("<br><br> Fichier %d : <br>%s<br>", $fileNb, $file);
}
/**
* Change order.
*
* @param \App\Http\Requests\Bo\UpdateChangeOrderRequest $request
* @param mixed ...$params
* @return \Illuminate\Http\RedirectResponse
* @throws \RuntimeException Si le nom du parametre de la route ne correspond pas au params utilisé.
*/
public function changeOrder(UpdateChangeOrderRequest $request, mixed ...$params): \Illuminate\Http\RedirectResponse
{
$paramNames = collect(request()->route()->parameterNames);
$className = Str::of($paramNames->get($paramNames->count() - 2))->lower()->studly();
$className = "\\App\\Models\\{$className}";
$model = Arr::get($params, $paramNames->count() - 2);
if (!class_exists($className)) {
throw new \RuntimeException("The {$className} class doesn't exist.");
}
if (!\is_object($model)) {
throw new \RuntimeException("The second last param isn't an object.");
}
if (get_class($model) !== (string)Str::of($className)->replaceStart('\\', '')) {
throw new \RuntimeException("The {$className} doesn't match second last binded param.");
}
/** @var \Illuminate\Database\Eloquent\Model|null */
$model = $className::query()->where($model->getRouteKeyName(), $model->getRouteKey())->first();
if (!$model) {
return back()->with('error', trans('crud.messages.order_not_changed'));
}
return DB::transaction(function () use ($request, $className, $model) {
/** @var \Illuminate\Support\Collection $models */
$models = $className::query()
->orderBy('order', 'ASC')
->where('site_id', $model->site_id)
->where('status', '!=', TicketStatusEnum::archived)
->where('id', '!=', $model->getKey())
->where('order', $request->direction ? '<' : '>', $model->order)
->get();
/** @var \Illuminate\Database\Eloquent\Model|null $tmp */
$tmp = $request->direction ? $models->last() : $models->first();
if (!$tmp) {
return back()->with('error', trans('crud.messages.order_not_changed'));
}
$newOrder = $tmp->getOriginal('order');
$tmp->update(['order' => $model->getOriginal('order')]);
$model->update(['order' => $newOrder]);
return redirect()->back()->with('success', trans('crud.messages.order_changed'));
});
}
/**
* Update ticket priority.
*
* @param UpdateTicketPriorityRequest $request
* @param Ticket $ticket
* @return void
*/
public function changePriority(UpdateTicketPriorityRequest $request, Ticket $ticket)
{
$newPriority = $request->validated('priority');
return DB::transaction(function () use ($ticket, $newPriority) {
// Update all tickets who will be moved with this priority update.
$request = Ticket::query()
->where('site_id', $ticket->site_id)
->whereNot('status', TicketStatusEnum::archived)
->whereNot('id', $ticket->id)
->whereBetween('order', [min($ticket->order, $newPriority), max($ticket->order, $newPriority)]);
$ticket->order < $newPriority ? $request->decrement('order', 1) : $request->increment('order', 1);
// Update the ticket.
$ticket->order = $newPriority;
if (!$ticket->saveOrFail()) {
return back()->with('error', trans('crud.messages.order_not_changed'));
};
return redirect()->back()->with('success', trans('crud.messages.order_changed'));
});
}
}

View file

@ -2,16 +2,15 @@
namespace App\Http\Controllers\Bo;
use App\Models\Site;
use App\Models\User;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Bo\Auth\ForgotPasswordController;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Users\StoreUserRequest;
use App\Http\Requests\Bo\Users\UpdateUserRequest;
use App\Http\Controllers\Bo\Auth\ForgotPasswordController;
use App\Http\Requests\Bo\Users\FavouriteSiteRequest;
use App\Models\Site;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class UserController extends Controller
{
@ -73,9 +72,8 @@ class UserController extends Controller
public function store(StoreUserRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$user = User::query()->onlyTrashed()->where('email', $request->validated('email'))->first() ?? new User();
$user = new User();
$user->fill($request->safe()->except('sites'));
$user->deleted_at = null;
if ($user->saveOrFail()) {
$user->sites()->attach(collect($request->validated('sites'))->pluck('id'));
@ -172,35 +170,9 @@ class UserController extends Controller
*
* @return \Illuminate\Contracts\View\View
*/
public function settings(): \Illuminate\Contracts\View\View
public function passwordReset(): \Illuminate\Contracts\View\View
{
return view('bo.pages.users.settings');
}
/**
* Favourite site submit
*
* @param \App\Http\Requests\Bo\Users\FavouriteSiteRequest $request
*
* @return \Illuminate\Http\RedirectResponse
*/
public function favouriteSiteSubmit(FavouriteSiteRequest $request): \Illuminate\Http\RedirectResponse
{
/** @var \App\Models\User */
$user = auth()->user();
try {
$user->favourite_site_id = $request->favourite_site_id;
$user->saveOrFail();
return redirect()->back()->with("success", trans('crud.messages.favourite_site_changed'));
} catch (\Exception $e) {
\report($e);
return redirect()->back()->with('error', trans('crud.messages.favourite_site_not_changed'));
} catch (\Throwable $e) {
\report($e);
return redirect()->back()->with('error', trans('crud.messages.favourite_site_not_changed'));
}
return view('bo.pages.users.passwordReset');
}
/**

View file

@ -15,7 +15,6 @@ use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Str;
class Controller extends BaseController
{
@ -118,78 +117,26 @@ class Controller extends BaseController
* @param array $fields
* @return \Illuminate\Database\Eloquent\Builder
*/
private function searchOnFields(
Builder $query,
string $search,
array $fields
): \Illuminate\Database\Eloquent\Builder {
private function searchOnFields(Builder $query, string $search, array $fields): Builder
{
$table = $query->getModel()->getTable();
foreach ($fields as $field) {
$field = str_replace('?', '\?', $field);
if (is_array($field)) {
foreach ($fields as $f) {
$f = str_replace('?', '\?', $f);
if (is_array($f)) {
$query->orWhereRaw(
sprintf("UPPER(CONCAT(%s)) LIKE UPPER(?)", collect($field)->map(function ($q) use ($table) {
sprintf("UPPER(CONCAT(%s)) LIKE UPPER(?)", collect($f)->map(function ($q) use ($table) {
return "`$table`.`$q`";
})->implode(', \' \', ')),
['%' . htmlspecialchars($search) . '%']
['%' . filter_var($search, FILTER_SANITIZE_STRING) . '%']
);
} else {
$query->orWhereRaw(
"UPPER(`$table`.`$f`) LIKE UPPER(?)",
['%' . filter_var($search, FILTER_SANITIZE_STRING) . '%']
);
return $query;
}
// Get models woth enum's label.
if (is_array($this->getSearchValue($query, $search, $field))) {
foreach ($this->getSearchValue($query, $search, $field) as $array) {
$query->orWhereRaw(
"UPPER(`$table`.`$field`) LIKE UPPER(?)",
['%' . htmlspecialchars($array->value) . '%']
);
}
return $query;
}
// Get models with relation's name/label.
if (Str::endsWith($field, '_id')) {
$query->orWhereHas(Str::remove('_id', $field), function ($queryRelation) use ($field, $search) {
if (Schema::hasColumn(Str::remove('_id', $field) . 's', 'name')) {
$queryRelation->where('name', 'like', '%' . $search . '%');
} elseif (Schema::hasColumn(Str::remove('_id', $field) . 's', 'label')) {
$queryRelation->where('label', 'like', '%' . $search . '%');
}
});
return $query;
}
$query->orWhereRaw(
"UPPER(`$table`.`$field`) LIKE UPPER(?)",
['%' . htmlspecialchars($this->getSearchValue($query, $search, $field)) . '%']
);
} //end foreach
return $query;
}
/**
* Get the search value (on string or Enum).
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $search
* @param string $field
* @return string|array
*/
protected function getSearchValue(Builder $query, string $search, string $field): string|array
{
$cast = $query->getModel()->getCasts()[$field] ?? null;
if (
count($query->getModel()->getCasts()) and // Check if the model has casts.
$cast and // Check if there is cast about specific field.
enum_exists($cast) // Check if the cast is an enum.
) {
/** @var \App\Traits\Enums $cast */
// @phpstan-ignore-next-line
$cases = collect($cast::toArray());
$enum = $cases->filter(function ($item) use ($search) {
return preg_match('/' . Str::lower($search) . '/', Str::lower($item->label));
});
return $enum->toArray();
} else {
return $search;
}
return $query;
}
/**
@ -216,13 +163,17 @@ class Controller extends BaseController
Session::remove("$rName.sort_col");
Session::remove("$rName.sort_way");
}
if (
request()->sort_col &&
request()->sort_way &&
Schema::hasColumn($table, request()->sort_col) &&
(!isset($ignore) || (isset($ignore) && !in_array(request()->sort_col, $ignore)))
) {
$query = $query->orderBy("$table." . request()->sort_col, request()->sort_way);
if (request()->sort_col and request()->sort_way and Schema::hasColumn($table, request()->sort_col)) {
if (
!isset($ignore) or
(isset($ignore) and !in_array(request()->sort_col, $ignore))
) {
$query = $query->orderBy(\sprintf("$table.%s", request()->sort_col), request()->sort_way);
}
} elseif (Schema::hasColumn($table, 'order')) {
$query = $query->orderBy('order', 'ASC');
Session::put("$rName.sort_col", 'order');
Session::put("$rName.sort_way", 'asc');
}
return $query;
}

View file

@ -27,8 +27,8 @@ class RedirectIfAuthenticated
if (Auth::guard($guard)->check()) {
switch ($guard) {
case 'backend':
if (!$request->routeIs('bo.*') && !$request->routeIs('login')) {
continue 2;
if (!$request->routeIs('bo.*')) {
continue;
}
return \redirect()->route('bo.dashboard');
default:

View file

@ -12,7 +12,6 @@ class VerifyCsrfToken extends Middleware
* @var array<int, string>
*/
protected $except = [
'/csp-report',
'/brevo-in'
'/csp-report'
];
}

View file

@ -29,7 +29,7 @@ class AssignTicketToUserRequest extends FormRequest
return [
'assign' => [
'nullable', 'exists:users,id',
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
function ($attribute, $value, $fail) {
/** @var \App\Models\User $user */
if ($user = User::whereId($value)->first() and !$user->isAdmin()) {

View file

@ -1,50 +0,0 @@
<?php
namespace App\Http\Requests\Bo;
use Domain\Models\ValueObjects\Email;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Config;
class BrevoWebhookRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
public function getFromAddress(): Email
{
return new Email($this->string('items.0.From.Address'));
}
public function getToAddress(): Email
{
return new Email((string) $this->input('items.0.To.0.Address'));
}
public function getRawHtmlBody(): string
{
return $this->string('items.0.RawHtmlBody');
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$expectedFormAddress = Config::string('mail.brevo_webook.expected_from_address');
return [
'items' => ['required', 'array', 'min:1'],
'items.0.From.Address' => ['required', 'email:rfc,spoof'],
'items.0.To.*.Address' => ['required', 'email:rfc,spoof', "in:{$expectedFormAddress}"],
'items.0.RawHtmlBody' => ['required', 'string', 'min:10'],
];
}
}

View file

@ -29,7 +29,7 @@ class ChangeTicketStatus extends FormRequest
return [
'status' => [
'required',
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
function ($attribute, $value, $fail) {
if (!in_array($value, TicketStatusEnum::toValues())) {
$fail(__('Veuillez choisir entre :values', [

View file

@ -37,11 +37,9 @@ class CreateMessage extends FormRequest
*/
protected function prepareForValidation()
{
/** @var \App\Models\User */
$user = auth()->user();
$this->merge([
'subscribe' => $this->subscribe ? true : false,
'type' => $this->is_private ? MessageTypeEnum::private->value : MessageTypeEnum::public->value
'type' => $this->is_private ? MessageTypeEnum::private->value : MessageTypeEnum::public->value
]);
}
@ -55,24 +53,13 @@ class CreateMessage extends FormRequest
/** @var \App\Models\User */
$user = auth()->user();
return [
'subscribe' => 'required|boolean',
'message' => [
'subscribe' => 'required|boolean',
'message' => 'required|min:' . ($user->isAdmin() ? '5' : '20'),
'file_message' => 'sometimes|mimes:jpeg,jpg,png,pdf|max:2048',
'type' => [
'required',
'min:' . ($user->isAdmin() ? '5' : '20'),
function ($attribute, $value, $fail) {
$forbidden = ['merci de', 'merci de'];
foreach ($forbidden as $word) {
if (stripos(strtolower($value), $word) !== false) {
$fail(
"Le champs 'Message' contient la chaine de caractère non autorisée suivante : $word"
);
}
}
},
],
'file_messages' => 'sometimes|array',
'file_messages.*' => 'mimes:pptx,docx,xlsx,csv,txt,jpeg,jpg,png,pdf,svg|max:2048',
'type' => ['required', new Enum(MessageTypeEnum::class)]
new Enum(MessageTypeEnum::class)
]
];
}
}

View file

@ -33,18 +33,14 @@ class CreateEstimateRequest extends FormRequest
$ticket = $this->ticket;
return [
'amount' => [
'required',
'numeric',
'gt:0',
function ($attribute, $value, $fail) {
if (fmod((\floatval($value) * 1000), 62.5) !== 0.0) {
$fail("$attribute doit être un multiple de 62.5 (soit 0.0625)");
'required', 'numeric', 'gt:0', function ($attribute, $value, $fail) {
if ((\floatval($value) * 1000) % 125 !== 0) {
$fail("$attribute doit être un multiple de 125 (soit 0.125)");
}
}
],
'credit_account_id' => [
'required',
'exists:credit_accounts,id',
'required', 'exists:credit_accounts,id',
function ($attribute, $value, $fail) use ($ticket) {
/** @var \App\Models\Credit\Account|null */
$account = Account::whereId($value)->first();
@ -61,11 +57,11 @@ class CreateEstimateRequest extends FormRequest
*
* @return array
*/
public function attributes(): array
public function messages(): array
{
return [
'amount' => 'Montant',
'credit_account_id' => 'Compte à débiter'
'credit_account_id' => 'Compte'
];
}
}

View file

@ -26,7 +26,7 @@ class StoreCreditLogRequest extends FormRequest
{
return [
'amount' => [
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
'required', 'numeric', 'min:0', 'max:255', function ($attribute, $value, $fail) {
if (($value * 10) % 5 !== 0) {
$fail("Le Montant de l'opération doit être divisible par 0.5 jour/homme");

View file

@ -0,0 +1,47 @@
<?php
namespace App\Http\Requests\Bo\Dutyhours;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
class StoreDutyhourRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize()
{
return Gate::allows('isAdmin');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'date' => 'required|date|after_or_equal:today',
'person' => 'required|string|min:3|max:255',
'place' => 'required|string|min:3|max:255',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes(): array
{
return [
'date' => trans('Date de la permanence'),
'person' => trans('Personne associée à la permanence'),
'place' => trans('Lieu de la permanence'),
];
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace App\Http\Requests\Bo\Dutyhours;
class UpdateDutyhourRequest extends StoreDutyhourRequest
{
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\Http\Requests\Bo;
use App\Enums\Planning\TableRowEnum;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
class EraseCellContentRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize()
{
return auth()->check() and $this->ajax();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'id' => 'required|exists:week_plannings,id',
'day' => 'required|numeric',
'row' => [
'required',
new Enum(TableRowEnum::class)
],
];
}
}

View file

@ -28,7 +28,7 @@ class StoreInvoiceRequest extends FormRequest
protected function prepareForValidation()
{
$this->merge([
'tasks' => request()->tasks ? \collect(json_decode(request()->tasks))
'tasks' => request()->tasks ? \collect(json_decode(request()->tasks))
->map(function ($task) {
return \array_intersect_key((array)$task, Invoice::getTaskStructure());
})->toArray() : [],
@ -44,24 +44,24 @@ class StoreInvoiceRequest extends FormRequest
public function rules()
{
return [
'firstname' => 'required|string|min:3|max:255',
'lastname' => 'required|string|min:3|max:255',
'object' => 'required|string|min:3|max:255',
'tasks' => 'required|array|min:1',
'tasks.*' => 'required|array',
'tasks.*.days' => 'required|numeric',
'tasks.*.amount' => 'required|numeric',
'tasks.*.desc' => 'required|string|min:10',
'tasks.*.type' => [
'firstname' => 'required|string|min:3|max:255',
'lastname' => 'required|string|min:3|max:255',
'object' => 'required|string|min:3|max:255',
'tasks' => 'required|array|min:1',
'tasks.*' => 'required|array',
'tasks.*.days' => 'required|numeric|min:0.125',
'tasks.*.amount' => 'required|numeric|min:0.125',
'tasks.*.desc' => 'required|string|min:10',
'tasks.*.type' => [
'required', new Enum(TaskTypeEnum::class)
],
'tasks.*.package' => 'required_if:tasks.*.type,' . TaskTypeEnum::package->value .
'|nullable|exists:packages,id',
'comment' => 'sometimes|nullable|string|min:15',
'show_days' => 'required|boolean',
'site_id' => 'required|integer|exists:sites,id',
'company_id' => 'required|integer|exists:companies,id',
'invoiced_to_id' => 'required|integer|exists:companies,id',
'comment' => 'sometimes|nullable|string|min:15',
'show_days' => 'required|boolean',
'site_id' => 'required|integer|exists:sites,id',
'company_id' => 'required|integer|exists:companies,id',
'invoiced_to_id' => 'required|integer|exists:companies,id',
];
}

View file

@ -21,14 +21,13 @@ class UpdateInvoiceRequest extends StoreInvoiceRequest
$rules = array_merge($rules, parent::rules());
}
$rules = array_merge($rules, [
'comment' => 'sometimes|nullable|string|min:15',
'sent_client_date' => 'required_with:signed_date,sent_accounting_date|sometimes|nullable|date',
'signed_date' => 'required_with:sent_accounting_date|sometimes|nullable|date',
'sent_client_date' => 'required_with:signed_date,sent_accounting_date|sometimes|nullable|date',
'signed_date' => 'required_with:sent_accounting_date|sometimes|nullable|date',
'sent_accounting_date' => 'sometimes|nullable|date',
'status' => [
'status' => [
'required',
new Enum(InvoiceStatusEnum::class),
//phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
//phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
function ($attribute, $value, $fail) use ($model) {
if (
(intval($value) === InvoiceStatusEnum::archived->value) &&

View file

@ -2,9 +2,9 @@
namespace App\Http\Requests\Bo\Labels;
use App\Rules\Hex;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use LVR\Colour\Hex;
class StoreLabelRequest extends FormRequest
{

View file

@ -1,39 +0,0 @@
<?php
namespace App\Http\Requests\Bo;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class LinkTicket extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
/** @var \App\Models\User|null $user */
$user = Auth::user();
return Auth::check() && $user->isAdmin();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
$ticket = $this->route('ticket');
return [
'linked_ticket_id' => [
'required',
'numeric',
'exists:tickets,id',
'not_in:' . $ticket->id,
]
];
}
}

View file

@ -17,18 +17,6 @@ class StorePackageRequest extends FormRequest
return Gate::allows('isAdmin');
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge([
'published' => $this->boolean('published'),
]);
}
/**
* Get the validation rules that apply to the request.
*
@ -39,7 +27,6 @@ class StorePackageRequest extends FormRequest
return [
'name' => 'required|string|min:3|max:255',
'amount' => 'required|numeric|min:0|regex:/^\d+(\.\d{1,2})?$/',
'published' => 'required|boolean',
];
}
@ -53,7 +40,6 @@ class StorePackageRequest extends FormRequest
return [
'name' => trans('Nom'),
'amount' => trans('Montant (par jour)'),
'published' => trans('Publication')
];
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests\Bo\Projects;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
class StoreProjectRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize()
{
return Gate::allows('isAdmin');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required|string|min:3|max:255|unique:projects,name',
'site_id' => 'required|exists:sites,id',
];
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Bo\Projects;
class UpdateProjectRequest extends StoreProjectRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return array_merge(parent::rules(), [
'name' => 'required|string|min:3|max:255|unique:projects,name,' . $this->route('project')->id . ',id',
]);
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace App\Http\Requests\Bo;
use App\Enums\Planning\CellModelEnum;
use App\Enums\Planning\CellTypeEnum;
use App\Enums\Planning\TableRowEnum;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
class SetCellContentRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
$this->merge([
'model' => $this->model ?? null,
'foreign' => $this->foreign ?? null,
'text' => $this->text ?? ''
]);
return auth()->check() and $this->ajax();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'id' => 'required|exists:week_plannings,id',
'day' => 'required|numeric',
'row' => [
'required',
new Enum(TableRowEnum::class)
],
'type' => [
'required',
new Enum(CellTypeEnum::class)
],
'CellModelEnum' => \sprintf(
'nullable',
new Enum(CellModelEnum::class)
),
'foreign' => 'required_with:CellModelEnum|nullable|numeric',
'text' => 'required|string|min:5|max:90'
];
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes(): array
{
return [
'CellModelEnum' => 'Type d\'élément lié',
'foreign' => 'Élément lié',
'text' => 'Description courte'
];
}
}

View file

@ -28,8 +28,7 @@ class StoreSiteRequest extends FormRequest
'name' => 'required|string|min:3|max:255',
'users' => 'nullable|array',
'users.*.id' => 'required|exists:users,id',
'gitlab_project_id' => 'nullable',
'project_manager_id' => 'nullable|exists:users,id',
'gitlab_project_id' => 'nullable|unique:sites,gitlab_project_id',
'url_re7' => 'nullable|url',
'url_prod' => 'nullable|url'
];
@ -47,7 +46,6 @@ class StoreSiteRequest extends FormRequest
'users' => trans('Utilisateurs associés au site'),
'users.*.id' => trans('Élément parmis les Utilisateurs associés au site'),
'gitlab_project_id' => trans('ID du dépôt Gitlab'),
'project_manager_id' => trans('Chef de projet'),
'url_re7' => trans('Url de RECETTE'),
'url_prod' => trans('Url de prod')
];

View file

@ -4,4 +4,15 @@ namespace App\Http\Requests\Bo\Sites;
class UpdateSiteRequest extends StoreSiteRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return array_merge(parent::rules(), [
'gitlab_project_id' => 'nullable|unique:sites,gitlab_project_id,' . $this->route('site')->id . ',id',
]);
}
}

View file

@ -29,7 +29,7 @@ class SortTickets extends FormRequest
'sort_way' => 'sometimes|in:asc,desc',
'status' => [
'sometimes',
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
function ($attribute, $value, $fail) {
if (!in_array($value, TicketStatusEnum::toValues())) {
$fail(__('Veuillez choisir entre :values', [

View file

@ -60,7 +60,7 @@ class StoreTicket extends FormRequest
'site_id' => [
'required',
'exists:sites,id',
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
function ($attribute, $value, $fail) {
/** @var \App\Models\User */
$user = auth()->user();
@ -76,7 +76,7 @@ class StoreTicket extends FormRequest
],
'type' => [
'required',
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
function ($attribute, $value, $fail) {
if (!in_array($value, TicketTypeEnum::toValues())) {
$fail(__('Veuillez choisir entre :values', [
@ -87,7 +87,7 @@ class StoreTicket extends FormRequest
],
'status' => [
'required',
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
function ($attribute, $value, $fail) {
if (!in_array($value, TicketStatusEnum::toValues())) {
$fail(__('Veuillez choisir entre :values', [
@ -96,29 +96,13 @@ class StoreTicket extends FormRequest
}
}
],
'date_mep' => 'nullable|date',
'text' => [
'required',
'string',
'min:40',
function ($attribute, $value, $fail) {
$forbidden = ['merci de', 'merci de'];
foreach ($forbidden as $word) {
if (stripos(strtolower($value), $word) !== false) {
$fail(
"Le champs 'Description du ticket' contient la chaine de caractère non autorisée suivante : $word"
);
}
}
},
],
'title' => 'required|string|between:3,255',
'file_tickets' => 'sometimes|array',
'file_tickets.*' => 'mimes:pptx,docx,xlsx,csv,txt,jpeg,jpg,png,pdf,svg|max:2048',
'is_internal' => 'required|boolean',
'labels' => 'sometimes|array',
'labels.*' => [
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
'text' => 'required|string|min:40',
'file_ticket' => 'mimes:jpeg,jpg,png,pdf|max:2048',
'file_ticket2' => 'mimes:jpeg,jpg,png,pdf|max:2048',
'is_internal' => 'required|boolean',
'labels' => 'sometimes|array',
'labels.*' => [
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
'required', 'exists:labels,id', function ($attribute, $value, $fail) use ($user) {
if (!$user->can('assignLabels', Ticket::class)) {
$fail(trans('Vous n\'êtes pas autorisé à modifier cette valeur'));

View file

@ -1,42 +0,0 @@
<?php
namespace App\Http\Requests\Bo;
use Illuminate\Foundation\Http\FormRequest;
class UpdateChangeOrderRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth()->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge([
'direction' => $this->route('direction') === 'up' ? true : false
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'direction' => 'required|boolean',
];
}
}

View file

@ -1,57 +0,0 @@
<?php
namespace App\Http\Requests\Bo;
use App\Models\Ticket;
use Illuminate\Foundation\Http\FormRequest;
class UpdateTicketPriorityRequest extends FormRequest
{
private \App\Models\Ticket $ticket;
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth()->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->ticket = $this->route('ticket');
$max = Ticket::query()->where('site_id', $this->ticket->site_id)->max('order');
$this->merge([
'priority' => min($this->integer('priority'), $max)
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'priority' => [
'required',
'integer',
'min:1',
function ($attribute, $value, $fail) {
if ($this->ticket && intval($value) === $this->ticket->order) {
$fail("La valeur de {$attribute} doit être différente de la valeur actuelle.");
}
},
],
];
}
}

View file

@ -1,39 +0,0 @@
<?php
namespace App\Http\Requests\Bo\Users;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Http\FormRequest;
class FavouriteSiteRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return Auth::check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
/** @var \App\Models\User|null $user */
$user = Auth::user();
return [
'favourite_site_id' => [
'nullable',
'exists:sites,id',
Rule::in($user->sites->pluck('id')),
],
];
}
}

View file

@ -19,18 +19,6 @@ class StoreUserRequest extends FormRequest
return Gate::allows('isAdmin');
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge([
'gitlab_id' => \boolval($this->input('gitlab_id')) ? $this->input('gitlab_id') : null
]);
}
/**
* Get the validation rules that apply to the request.
*
@ -40,18 +28,13 @@ class StoreUserRequest extends FormRequest
{
return [
'name' => 'required|string|min:3|max:255',
'email' => [
'email',
'required',
Rule::unique('users', 'email')->whereNull('deleted_at')
],
'email' => 'email|required|unique:users,email',
'role' => [
'required',
Rule::in(collect(RoleEnum::cases())->pluck('value')->all()),
],
'sites' => 'nullable|array',
'sites.*.id' => 'required|exists:sites,id',
'gitlab_id' => 'nullable|integer|unique:users,gitlab_id'
'sites.*.id' => 'required|exists:sites,id'
];
}
@ -68,7 +51,6 @@ class StoreUserRequest extends FormRequest
'role' => trans('Rôle de l\'utilisateur'),
'sites' => trans('Sites associés à l\'utilisateur'),
'sites.*.id' => trans('Élément parmis les sites associés à l\'utilisateur'),
'gitlab_id' => trans("ID de l'utilisateur Gitlab")
];
}
}

View file

@ -12,8 +12,7 @@ class UpdateUserRequest extends StoreUserRequest
public function rules()
{
return array_merge(parent::rules(), [
'email' => 'email|required|unique:users,email,' . $this->route('user')->id . ',id',
'gitlab_id' => 'nullable|integer|unique:users,gitlab_id,' . $this->route('user')->id . ',id'
'email' => 'email|required|unique:users,email,' . $this->route('user')->id . ',id',
]);
}
}

View file

@ -1,42 +0,0 @@
<?php
namespace App\Interfaces;
interface SpsConfigInterface
{
/**
* URL method
* Website (SP) url ex: https://le-gaulois-pro.s-web.test
*
* @return string
*/
public function url(): string;
/**
* acsBase64 method
* Return destination url base64 encoded
*
* @return string
*/
public function acsBase64(): string;
/**
* destination method
* Return acs url concatenated with url
*
* @return string
*/
public function destination(): string;
/**
* logout method
* Return sls url concatenated with url
*
* @return string
*/
public function logout(): string;
/**
* certificate method
* Return certificate (.pem) file from kebab case of Enum name
*
* @return string
*/
public function certificate(): string;
}

View file

@ -1,117 +0,0 @@
<?php
namespace App\Jobs;
use App\Enums\Tickets\TicketStatusEnum;
use App\Models\Site;
use App\Models\Ticket;
use App\Notifications\ExportTicketsStateNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Notification;
class ExportTicketsState implements ShouldQueue
{
use Queueable;
/**
* CSV File headers
*
* @var array
*/
private $header = [
"Priorité service",
"Site",
"Numéro de ticket",
"Priorité",
"Date de MEP souhaitée",
"Type de ticket",
"Titre du ticket",
"Statut du ticket",
"Chef de projet",
"Utilisateur assigné",
"Date de dernière activité",
"Date de création",
"Lien vers le ticket",
"Lien vers l'issue GitLab"
];
/**
* Execute the job.
*
* @return void
*/
public function handle(): void
{
// L'ordre commence à 0 et reprendra à 1 pour chaque site.
$order = 0;
// Dernier site traité.
$lastSiteId = null;
// Insertion de la ligne d'en-têtes.
$csvlines[] = $this->header;
// Récupération des tickets.
Ticket::query()
->whereNotIn(
'status',
[TicketStatusEnum::archived, TicketStatusEnum::production]
)
->orderBy(Site::select('name')
->whereColumn('sites.id', 'tickets.site_id'))
->orderBy('order')->get()->each(function (Ticket $ticket) use (&$csvlines, &$lastSiteId, &$order) {
$csvlines[] = [
"",
$ticket->site->name,
$ticket->id,
$lastSiteId === $ticket->site_id ? $order + 1 : 1,
$ticket->date_mep?->format('d-m-Y') ?? 'No MEP Date',
$ticket->type->label(),
$ticket->title,
$ticket->status->label(),
$ticket->projectManager->name ?? 'No PM',
$ticket->assignedUser()->name ?? 'No Assigned User',
Carbon::create($ticket->getRawOriginal('last_activity'))->format('d-m-Y H:i:s'),
$ticket->created_at->format('d-m-Y H:i:s'),
route('bo.ticket.show', $ticket->id),
$ticket->gitlab_issue ?? 'No GitLab Issue'
];
$order = $lastSiteId === $ticket->site_id ? $order + 1 : 1;
$lastSiteId = $ticket->site_id;
});
Notification::route('mail', config('mail.MAIL_ADMIN'))
->notify(new ExportTicketsStateNotification($this->array2csv($csvlines, ";")));
}
/**
* converts an array to a csv stream
*
* @param array $data
* @param string $delimiter
* @param string $enclosure
* @param string $escape_char
*
* @return string
*/
protected function array2csv(
array $data,
string $delimiter = ',',
string $enclosure = '"',
string $escape_char = "\\"
) {
$f = fopen('php://memory', 'r+');
// UTF-8 BOM.
$BOM = "\xEF\xBB\xBF";
fwrite($f, $BOM);
foreach ($data as $item) {
fputcsv($f, $item, $delimiter, $enclosure, $escape_char);
}
rewind($f);
return stream_get_contents($f);
}
}

View file

@ -1,85 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Ticket;
use App\Enums\Tickets\TicketStatusEnum;
use Illuminate\Foundation\Queue\Queueable;
use App\Traits\Controllers\HasGitlabIssue;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Http\Requests\Bo\ChangeTicketStatus;
class UpdateIssueLabels implements ShouldQueue
{
use Queueable;
use HasGitlabIssue;
/** @var Ticket */
protected $ticket;
/** @var requestStatus */
protected $requestStatus;
/**
* Create a new job instance.
*
* @param \App\Models\Ticket $ticket
* @param string $request
*
* @return void
*/
public function __construct(Ticket $ticket, string $requestStatus)
{
$this->ticket = $ticket;
$this->requestStatus = $requestStatus;
}
/**
* Execute the job.
*/
public function handle(): void
{
$oldStatus = $this->ticket->status;
$requestStatus = (int) $this->requestStatus;
$gitlabIssue = $this->getGitlabIssue($this->ticket->site->gitlab_project_id, $this->ticket->gitlab_issue);
$issueLabels = $gitlabIssue['labels'];
if ($oldStatus === TicketStatusEnum::waiting && $requestStatus !== TicketStatusEnum::waiting->value) {
$issueLabels = array_values(array_diff($issueLabels, ['g-wait']));
}
switch ($requestStatus) {
case TicketStatusEnum::takenInAccount->value:
$newLabels = ['g-to-dispatch'];
$updatedLabels = array_unique(array_merge($issueLabels, $newLabels));
$updatedIssue = $this->updateGitlabIssueLabels($gitlabIssue['project_id'], $gitlabIssue['iid'], $updatedLabels);
break;
case TicketStatusEnum::waiting->value:
$newLabels = ['g-wait'];
$updatedLabels = array_unique(array_merge($issueLabels, $newLabels));
$updatedIssue = $this->updateGitlabIssueLabels($gitlabIssue['project_id'], $gitlabIssue['iid'], $updatedLabels);
break;
case TicketStatusEnum::waitingValidation->value:
$removeLabels = ['g-to-estimate'];
$updatedLabels = array_diff($issueLabels, $removeLabels);
$updatedIssue = $this->updateGitlabIssueLabels($gitlabIssue['project_id'], $gitlabIssue['iid'], $updatedLabels);
break;
case TicketStatusEnum::recetteValidated->value:
$newLabels = ['g-re7-ok', 'to-prod'];
$updatedLabels = array_unique(array_merge($issueLabels, $newLabels));
$updatedIssue = $this->updateGitlabIssueLabels($gitlabIssue['project_id'], $gitlabIssue['iid'], $updatedLabels);
break;
case TicketStatusEnum::archived->value:
$updatedIssue = $this->closeGitlabIssue($gitlabIssue['project_id'], $gitlabIssue['iid'], $issueLabels);
break;
default:
$updatedIssue = $this->updateGitlabIssueLabels($gitlabIssue['project_id'], $gitlabIssue['iid'], $issueLabels);
break;
}
}
}

View file

@ -68,18 +68,6 @@ class ToolboxHelper
return \preg_match('/(\.png|\.jpg|\.jpeg|\.gif)$/', $url, $m) === 1;
}
/**
* Check uri points to pdf file
*
* @param string $url
* @return boolean
*/
public static function isPdfFile(string $url): bool
{
$m = [];
return \preg_match('/(\.pdf)$/', $url, $m) === 1;
}
/**
* Strip scheme (https) on oembed urls
* Only embed allowed are for MP4 self hosted videos right now
@ -351,7 +339,7 @@ class ToolboxHelper
): Builder {
$currentLocale = app()->currentLocale();
$locales = collect(config('app.locales'))
->mapWithKeys(fn(string $locale) => [$locale => $locale]);
->mapWithKeys(fn (string $locale) => [$locale => $locale]);
if (!$locales->has($defaultLocale)) {
// * Remove default locale.
$locales->pull($defaultLocale);
@ -369,7 +357,7 @@ class ToolboxHelper
$q->whereRaw("JSON_EXTRACT({$column}, '$.{$currentLocale}') = '{$value}'");
}
// * Query any other locale
$locales->each(fn($locale) => $q->orWhereRaw(
$locales->each(fn ($locale) => $q->orWhereRaw(
"JSON_EXTRACT({$column}, '$.{$locale}') = '{$value}'"
));
});
@ -461,10 +449,7 @@ class ToolboxHelper
$fileFullPath = self::getUploadedFileFullPath($value, $useStorage);
if (
!\in_array(self::getUploadedFileMimeType($value, $useStorage), [
'image/jpeg',
'image/jpg',
'image/png',
'image/gif'
'image/jpeg', 'image/jpg', 'image/png', 'image/gif'
])
) {
$fail(trans(':attribute doit être une image de type \'image/jpeg,image/jpg,image/png,image/gif\'', [

View file

@ -22,7 +22,8 @@ class AddStatusMessages
$message = new Message();
$message->ticket_id = $event->ticket->id;
// * Le message system est privé si demandé (eg: Assignation à une personne).
$message->type = $event->is_private ? MessageTypeEnum::private : MessageTypeEnum::public;
$message->type = $event->is_private ?
MessageTypeEnum::public : MessageTypeEnum::private;
if ($event->message) {
$message->message = $event->message;
} elseif ($event->user) {

View file

@ -1,75 +0,0 @@
<?php
namespace App\Listeners;
use App\Events\BrevoWebhookCallReceived;
use App\Events\TicketCreated;
use App\Mail\NewTicket;
use App\Repositories\SiteRepository;
use App\Services\AutomaticTicketCreator;
use Domain\Models\ValueObjects\Email;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail;
use Psr\Log\LoggerInterface;
// NOTE: this should be queued in order to be eventually retried or at least keep info data.
class CreateTicketAutomatically implements ShouldQueue
{
private const string SITE_NAME_WHERE_TO_CREATE_TICKET = 'Mailing en masse';
public function __construct(
private readonly LoggerInterface $logger,
private readonly AutomaticTicketCreator $automaticTicketCreator,
private readonly SiteRepository $siteRepository,
private readonly Dispatcher $dispatcher
) {}
public function handle(BrevoWebhookCallReceived $event): void
{
$fromEmail = $event->from;
$toEmail = $event->to;
$htmlRawContent = $event->htmlRawContent;
$this->logger->info('Received a ticket creation event from brevo');
$this->logger->debug(
'Payload for ticket creation form brevo',
[
'from_email' => $fromEmail->value,
'to_email' => $toEmail->value,
'html_raw_content' => base64_encode($htmlRawContent)
]
);
$siteReceivingTickets = $this->siteRepository->findByName(self::SITE_NAME_WHERE_TO_CREATE_TICKET);
if ($siteReceivingTickets === null) {
throw new \UnexpectedValueException(
'No site found with name \'' .
self::SITE_NAME_WHERE_TO_CREATE_TICKET . '\''
);
}
$projectManager = $siteReceivingTickets->projectManager()->first();
if ($projectManager === null) {
throw new \UnexpectedValueException(
'No project manager found for site \'' .
self::SITE_NAME_WHERE_TO_CREATE_TICKET . '\''
);
}
$ticket = $this->automaticTicketCreator->createTicket(
$siteReceivingTickets,
$projectManager,
$htmlRawContent,
);
$this->dispatcher->dispatch(new TicketCreated(
siteReceivingTicket: $siteReceivingTickets,
ticket: $ticket,
byUserName: $fromEmail->value
));
}
}

View file

@ -3,51 +3,21 @@
namespace App\Listeners;
use App\Events\NotifyAdmin;
use App\Events\TicketCreated;
use App\Mail\NewTicket;
use App\Mail\TicketUpdate;
use Domain\Models\ValueObjects\Email;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail;
class SendNotificationMailToAdmin
{
private readonly Email $mailAdmin;
public function __construct()
{
$this->mailAdmin = new Email(
Config::string('mail.MAIL_ADMIN')
);
}
/**
* Handle the event.
*
* @param \App\Events\NotifyAdmin $event
* @return void
*/
public function handle(NotifyAdmin | TicketCreated $event)
public function handle(NotifyAdmin $event)
{
if ($event instanceof NotifyAdmin) {
$ticket = $event->ticket;
Mail::to($this->mailAdmin->value)
->send(new TicketUpdate($ticket, $event->corpsMaj));
}
if ($event instanceof TicketCreated) {
$ticket = $event->ticket;
$siteReceivingTickets = $event->siteReceivingTicket;
$userName = $event->byUserName;
Mail::to($this->mailAdmin->value)
->send(new NewTicket(
ticketId: $ticket->id,
siteName: $siteReceivingTickets->name,
ticketTitle: $ticket->title,
ticketType: $ticket->type,
userName: $userName,
ticketDescription: $ticket->text,
));
}
$ticket = $event->ticket;
Mail::to(config('mail.MAIL_ADMIN'))
->send(new TicketUpdate($ticket, $event->corpsMaj));
}
}

View file

@ -2,23 +2,29 @@
namespace App\Mail;
use App\Enums\Tickets\TicketTypeEnum;
use App\Models\Ticket;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class NewTicket extends Mailable implements ShouldQueue
class NewTicket extends Mailable
{
use Queueable;
use SerializesModels;
public function __construct(
private readonly int $ticketId,
private readonly string $siteName,
private readonly string $ticketTitle,
private readonly TicketTypeEnum $ticketType,
private readonly string $userName,
private readonly string $ticketDescription,
) {}
/** @var \App\Models\Ticket */
private $ticket;
/**
* Create a new message instance.
*
* @param \App\Models\Ticket $ticket
* @return void
*/
public function __construct(Ticket $ticket)
{
$this->ticket = $ticket;
}
/**
* Build the message.
@ -27,14 +33,8 @@ class NewTicket extends Mailable implements ShouldQueue
*/
public function build()
{
return $this->subject("[Support] Nouveau ticket n° {$this->ticketId} - {$this->ticketTitle}")
->view('emails.new-ticket', [
'ticketId' => $this->ticketId,
'siteName' => $this->siteName,
'ticketTitle' => $this->ticketTitle,
'ticketType' => $this->ticketType,
'userName' => $this->userName,
'ticketDescription' => $this->ticketDescription,
]);
$ticket = $this->ticket;
return $this->subject("[Support] Nouveau ticket n°" . $ticket->id)
->view('emails.new-ticket', compact('ticket'));
}
}

View file

@ -41,7 +41,7 @@ class TicketMention extends Mailable
{
$ticket = $this->ticket;
$corpsMaj = ToolboxHelper::plainToHtml($this->corpsMaj);
return $this->subject("[Support] Vous avez été mentionné sur le ticket n°$ticket->id - $ticket->title")
->view('emails.ticket-mention', compact('ticket', 'corpsMaj'));
return $this->subject("[Support] Vous avez été mentionné sur le ticket n° $ticket->id")
->view('emails.ticket-mention', compact('ticket', 'corpsMaj'));
}
}

View file

@ -41,7 +41,7 @@ class TicketUpdate extends Mailable
{
$ticket = $this->ticket;
$corpsMaj = \html_entity_decode(ToolboxHelper::plainToHtml($this->corpsMaj));
return $this->subject("[Support][{$ticket->site->name}] Ticket n°$ticket->id - $ticket->title")
->view('emails.ticket-update', compact('ticket', 'corpsMaj'));
return $this->subject("[Support][{$ticket->site->name}] Ticket n°$ticket->id")
->view('emails.ticket-update', compact('ticket', 'corpsMaj'));
}
}

View file

@ -7,13 +7,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id Doit correspondre aux ids marques de la CRM
* @property string $name Nom de la marque
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, Account> $accounts
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Site> $sites
* @mixin \Eloquent
* @property string $name
*/
class Brand extends Model
{

View file

@ -7,14 +7,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
/**
* @property int $id
* @property string $name
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Invoice> $invoices
* @property-read mixed $value
* @method static \Database\Factories\CompanyFactory factory($count = null, $state = [])
* @mixin \Eloquent
*/
class Company extends Model
{

View file

@ -7,16 +7,10 @@ use App\Models\Brand;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property AccountTypeEnum $type Enum Credit\AccountTypeEnum
* @property string $label Custom account label
* @property float $balance Account balance
* @property int $brand_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read Brand $brand
* @property-read \App\Models\Credit\CreditLog|null $creditlogs
* @mixin \Eloquent
* @property string $label
* @property AccountTypeEnum $type
* @property integer $balance
* @property Brand $brand_id
*/
class Account extends Model
{

View file

@ -6,15 +6,10 @@ use App\Enums\Credit\BalanceOperationEnum;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property BalanceOperationEnum $type Enum Credit\BalanceOperationEnum
* @property float $amount Montant de l'opération
* @property int $credit_account_id
* @property string $created_at
* @property-read \App\Models\Credit\Account $account
* @property-read \App\Models\Credit\EstimationLog|null $pivot
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Credit\Estimation> $estimation
* @mixin \Eloquent
* @property BalanceOperationEnum $type
* @property double $amount
* @property Account $credit_account_id
* @property date $created_at
*/
class CreditLog extends Model
{

View file

@ -9,22 +9,6 @@ use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\ValidationException;
/**
* @property int $id
* @property EstimationStatusEnum $status Enum Credit\EstimationStatusEnum
* @property numeric $amount Estimation amount
* @property int $ticket_id
* @property int $credit_account_id The account to debit.
* @property int $estimator_id The person who has done the estimate.
* @property int|null $validator_id The person who has validate the estimate.
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Credit\Account $account
* @property-read User|null $estimator
* @property-read Ticket|null $ticket
* @property-read User|null $validator
* @mixin \Eloquent
*/
class Estimation extends Model
{
/**

View file

@ -4,14 +4,6 @@ namespace App\Models\Credit;
use Illuminate\Database\Eloquent\Relations\Pivot;
/**
* @property int $id
* @property int $credit_log_id
* @property int $credit_estimation_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @mixin \Eloquent
*/
class EstimationLog extends Pivot
{
/**

43
app/Models/Dutyhour.php Normal file
View file

@ -0,0 +1,43 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property date $date
* @property string $person
* @property string $place
*/
class Dutyhour extends Model
{
use HasFactory;
/**
* Indicates if the model should be timestamped.
*
* @var boolean
*/
public $timestamps = true;
/**
* The attributes that are fillable.
*
* @var array
*/
protected $fillable = [
'date',
'person',
'place'
];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $casts = [
'date' => 'date'
];
}

View file

@ -1,60 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $title Gitlab issue title
* @property string $url Gitlab URL
* @property int $estimate_time Gitlab issue estimate time in seconds
* @property int $spent_time Gitlab issue spent time in seconds
* @property int $is_closed Gitlab issue status
* @property int $gitlab_id Gitlab issue id
* @property int $gitlab_iid Gitlab issue iid in project
* @property int|null $milestone_iid Gitlab issue linked milestone iid in project
* @property int $gitlab_project_id Gitlab project id
* @property string $g_created_at Gitlab issue created at
* @property string $g_updated_at Gitlab issue updated at
* @property string|null $g_closed_at Gitlab issue closed at
* @property string|null $g_due_date Gitlab issue due date
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\GitlabLabels> $gitlabLabels
* @mixin \Eloquent
*/
class GitlabIssues extends Model
{
/**
* Fillable array model
*
* @var array
*/
protected $fillable = [
'title',
'url',
'estimate_time',
'spent_time',
'is_closed',
'gitlab_id',
'gitlab_iid',
'milestone_iid',
'gitlab_project_id',
'g_created_at',
'g_updated_at',
'g_closed_at',
'g_due_date'
];
/**
* Gitlab Labels relationship
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function gitlabLabels()
{
return $this->belongsToMany(GitlabLabels::class, 'gitlab_issues_labels', 'issue_id', 'label_id')
->withTimestamps();
}
}

View file

@ -1,38 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $name Label name
* @property int $gitlab_id Label Gitlab id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\GitlabIssues> $issues
* @mixin \Eloquent
*/
class GitlabLabels extends Model
{
/**
* Fillable array model
*
* @var array
*/
protected $fillable = [
'name',
'gitlab_id'
];
/**
* Gitlab Issues relationship
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function issues()
{
return $this->belongsToMany(GitlabIssues::class, 'gitlab_issues_labels', 'label_id', 'issue_id')
->withTimestamps();
}
}

View file

@ -1,42 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $title Milestone title
* @property string $url Gitlab URL
* @property int $is_closed Gitlab milestone status
* @property int $gitlab_id Gitlab milestone id
* @property int $gitlab_iid Gitlab milestone iid in project
* @property int $gitlab_project_id Gitlab project id
* @property string $g_created_at Gitlab milestone created at
* @property string $g_updated_at Gitlab milestone updated at
* @property string|null $g_start_date Gitlab milestone start date
* @property string|null $g_due_date Gitlab milestone due date
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @mixin \Eloquent
*/
class GitlabMilestones extends Model
{
/**
* Fillable array model
*
* @var array
*/
protected $fillable = [
'title',
'url',
'is_closed',
'gitlab_id',
'gitlab_iid',
'gitlab_project_id',
'g_created_at',
'g_updated_at',
'g_start_date',
'g_due_date'
];
}

View file

@ -1,30 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $name Gitlab project name
* @property string $url Gitlab URL
* @property int $is_archived Gitlab archive status
* @property int $gitlab_id Gitlab unique ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @mixin \Eloquent
*/
class GitlabProjects extends Model
{
/**
* Fillable array model
*
* @var array
*/
protected $fillable = [
'name',
'url',
'is_archived',
'gitlab_id'
];
}

View file

@ -7,13 +7,9 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $title
* @property string $message
* @property InfoColorsEnum $type Defined by the InfoColorsEnum enum.
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @mixin \Eloquent
* @property InfoColorsEnum $type
*/
class Information extends Model
{

View file

@ -10,42 +10,28 @@ use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Mccarlosen\LaravelMpdf\LaravelMpdf;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property int $id
* @property string $ref The invoice reference built as YYYY_MM_INCREMENT_USER. INCREMENT is the N invoice of the month. USER is the first two letters of the user who last edited the invoice. This field has to generated only on create.
* @property string $firstname The person to which the invoice is addressed.
* @property string $lastname The person to which the invoice is addressed.
* @property string $object The invoice object.
* @property array<array-key, mixed> $tasks A JSON object, each line has to be contructed using these fields description, days, type Invoices\TaskTypeEnum, amount
* @property float $total The total calculated using all tasks
* @property float $total_days The total days calculated using all tasks
* @property string|null $comment Any comment joined to the invoice
* @property int $show_days Show days on the printed invoice
* @property Carbon|null $signed_date The date the invoice was signed
* @property Carbon|null $sent_client_date The date the invoice was send to the client
* @property Carbon|null $sent_accounting_date The date the invoice was send to the client
* @property int $site_id
* @property int $company_id
* @property int $invoiced_to_id
* @property InvoiceStatusEnum $status Status of the invoice.
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property-read \App\Models\Company $company
* @property-read \App\Models\Company $companyInvoiced
* @property-read \App\Models\Site|null $site
* @method static \Database\Factories\InvoiceFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|Invoice onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Invoice withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Invoice withoutTrashed()
* @mixin \Eloquent
* @property string $ref
* @property string $firstname
* @property string $lastname
* @property string $object
* @property string $tasks
* @property double $total
* @property double $total_days
* @property string $comment
* @property boolean $show_days
* @property date $signed_date
* @property date $sent_client_date
* @property date $sent_accounting_date
* @property Site $site_id
* @property Company $company_id
* @property Company $invoiced_to_id
* @property InvoiceStatusEnum $status
*/
class Invoice extends Model
{
use HasFactory;
use SoftDeletes;
/**
* The attributes that are fillable.
@ -97,7 +83,7 @@ class Invoice extends Model
Carbon::now()->isoFormat('YYYY-MM'),
Str::padLeft((Invoice::whereYear('created_at', Carbon::now()->isoFormat('YYYY'))
->whereMonth('created_at', Carbon::now()->isoFormat('MM'))
->count() + 1) ?: 1, 4, '0'),
->count() + 1) ?: 1, 4, '0'),
Str::substr(Str::upper(Str::slug(Auth::user()->name)), 0, 1)
);
$model->total_days = collect($model->tasks)
@ -123,10 +109,6 @@ class Invoice extends Model
->reduce(function ($previous, array $task) {
return \floatval($task['amount']) + \floatval($previous);
});
if($model->sent_accounting_date && $model->status !== InvoiceStatusEnum::archived->value) {
$model->status = InvoiceStatusEnum::archived;
}
});
static::deleting(function () {
throw new \Error('Action impossible');
@ -210,30 +192,30 @@ class Invoice extends Model
'packages' => $packages,
],
[
'mode' => '',
'format' => 'A4',
'default_font_size' => '12',
'default_font' => 'sans-serif',
'margin_left' => 0,
'margin_right' => 0,
'margin_top' => 0,
'margin_bottom' => 0,
'margin_header' => 0,
'margin_footer' => 0,
'orientation' => 'P',
'title' => __('Devis Équipe Web :ref', ['ref' => $this->ref]),
'author' => 'Equipe Web',
'watermark' => '',
'show_watermark' => false,
'watermark_font' => 'sans-serif',
'display_mode' => 'fullpage',
'watermark_text_alpha' => 0.1,
'custom_font_dir' => '',
'mode' => '',
'format' => 'A4',
'default_font_size' => '12',
'default_font' => 'sans-serif',
'margin_left' => 0,
'margin_right' => 0,
'margin_top' => 0,
'margin_bottom' => 0,
'margin_header' => 0,
'margin_footer' => 0,
'orientation' => 'P',
'title' => __('Devis Équipe Web :ref', ['ref' => $this->ref]),
'author' => 'Equipe Web',
'watermark' => '',
'show_watermark' => false,
'watermark_font' => 'sans-serif',
'display_mode' => 'fullpage',
'watermark_text_alpha' => 0.1,
'custom_font_dir' => '',
'custom_font_data' => [],
'auto_language_detection' => false,
'temp_dir' => ini_get('upload_tmp_dir') ? ini_get('upload_tmp_dir') : sys_get_temp_dir(),
'pdfa' => false,
'pdfaauto' => false,
'auto_language_detection' => false,
'temp_dir' => ini_get('upload_tmp_dir') ? ini_get('upload_tmp_dir') : sys_get_temp_dir(),
'pdfa' => false,
'pdfaauto' => false,
]
);
return $pdf;

View file

@ -7,14 +7,6 @@ use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $name Un ou deux mot désignant le label
* @property string $color Cast HTML color
* @property-read \App\Models\Site|null $site
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Ticket> $tickets
* @mixin \Eloquent
*/
class Label extends Model
{
use HasFactory;

View file

@ -9,20 +9,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
*
* @property string $message The message content.
* @property \App\Enums\MessageTypeEnum $type The message is not visible for clients.
* @property string $file The message joined file path.
*
* @package App\Models
* @property int $id
* @property int $ticket_id
* @property int|null $user_id
* @property string $message
* @property MessageTypeEnum $type Message Type defined by an enum.
* @property string|null $file
* @property string|null $file2 Pièce jointes n°2
* @property string|null $file3 Pièce jointes n°3
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Ticket|null $ticket
* @property-read \App\Models\User|null $user
* @mixin \Eloquent
*/
class Message extends Model
{
@ -104,9 +96,14 @@ class Message extends Model
}
// ? Is message author is admin ?
if ($messageUser->isAdmin()) {
// Url: https://supportweb.ldc.fr/ticket/583/message/create.
// SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '583-8' for key 'PRIMARY'
// $ticket->subscriptions()->attach($messageUser);
// Add subscription.
$messageUser->subscriptions()->syncWithoutDetaching([$ticket->id]);
\session()->flash('info', trans('Vous avez été inscrit.'));
if (!$messageUser->subscriptions()->find($ticket->id)) {
$messageUser->subscriptions()->attach($ticket->id);
\session()->flash('info', trans('Vous avez été inscrit.'));
}
}
// * Send mail to other subscribed users
event(new NotifySubscriptions(

View file

@ -6,12 +6,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property string $name
* @property float $amount The cost per day
* @property bool $published
* @method static \Database\Factories\PackageFactory factory($count = null, $state = [])
* @mixin \Eloquent
* @property double $amount
*/
class Package extends Model
{
@ -32,16 +28,5 @@ class Package extends Model
protected $fillable = [
'name',
'amount',
'published'
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'published' => 'boolean',
];
}

37
app/Models/Project.php Normal file
View file

@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property string $name
* @property Site $site_id
*/
class Project extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'site_id'
];
// * RELATIONSHIPS.
/**
* BelongsTo Site
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function site()
{
return $this->belongsTo(Site::class);
}
}

View file

@ -6,30 +6,14 @@ use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property int $id
* @property string $name
* @property string|null $url_re7
* @property string|null $url_prod
* @property int|null $gitlab_project_id
* @property int|null $brand_id Relation is nullable, since not all site would be affiliate with a brand.
* @property int|null $project_manager_id Project manager relationship
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read \App\Models\Brand|null $brand
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Invoice> $invoices
* @property-read \App\Models\User|null $projectManager
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Ticket> $tickets
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $users
* @property-read mixed $value
* @method static \Illuminate\Database\Eloquent\Builder<static>|Site onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Site withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Site withoutTrashed()
* @mixin \Eloquent
* @property string $name
* @property string $url_re7
* @property string $url_prod
* @property integer $gitlab_project_id
* @property Brand $brand_id
*/
class Site extends Model
{
@ -45,8 +29,7 @@ class Site extends Model
'name',
'url_re7',
'url_prod',
'gitlab_project_id',
'project_manager_id'
'gitlab_project_id'
];
/**
@ -72,10 +55,6 @@ class Site extends Model
static::updating(function (self $model) {
self::manageFields($model);
});
static::deleted(function ($site) {
$site->invoices()->delete();
$site->tickets()->delete();
});
}
// * METHODS
@ -103,7 +82,7 @@ class Site extends Model
* HasMany Message relation
* The users of type client that would have access to this site tickets
*
* @return \Illuminate\Database\Eloquent\Relations\belongsToMany
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function users()
{
@ -120,16 +99,6 @@ class Site extends Model
return $this->hasMany(Ticket::class, 'site_id');
}
/**
* HasMany Invoices relation
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function invoices()
{
return $this->hasMany(Invoice::class, 'site_id');
}
/**
* BelongsTo Brand relation
*
@ -141,14 +110,13 @@ class Site extends Model
}
/**
* BelongsTo User relation
* The project manager of the site
* BelongsTo Project relation
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function projectManager(): BelongsTo
public function project()
{
return $this->belongsTo(User::class, 'project_manager_id');
return $this->belongsTo(Project::class);
}
/**

View file

@ -4,11 +4,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
/**
* @property int $site_id
* @property int $user_id
* @mixin \Eloquent
*/
class SiteUser extends Pivot
{
/**

View file

@ -2,68 +2,26 @@
namespace App\Models;
use App\Events\NotifyAdmin;
use App\Mail\TicketMention;
use Illuminate\Support\Str;
use App\Models\GitlabIssues;
use App\Enums\Users\RoleEnum;
use App\Models\Credit\Account;
use App\Models\Credit\Estimation;
use App\Models\Ticket\TicketType;
use Illuminate\Support\Collection;
use App\Enums\Tickets\TicketStatusEnum;
use App\Enums\Tickets\TicketTypeEnum;
use App\Events\NotifyAdmin;
use App\Events\NotifySubscriptions;
use App\Events\TicketStatusChanged;
use App\Jobs\SendTicketMentionMail;
use App\Enums\Tickets\TicketTypeEnum;
use App\Enums\Tickets\TicketStatusEnum;
use Illuminate\Database\Eloquent\Model;
use GrahamCampbell\GitLab\Facades\GitLab;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Mail\TicketMention;
use App\Models\Credit\Account;
use App\Models\Credit\Estimation;
use App\Models\Ticket\TicketType;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
/**
* @property int $id
* @property int $created_by
* @property int|null $assigned_to La personne a qui est assigné le ticket.
* @property int $site_id
* @property TicketTypeEnum $type
* @property string $title Titre du ticket
* @property string $text
* @property string|null $file
* @property string|null $file2
* @property string|null $file3 Pièce jointes n°3
* @property string|null $file4 Pièce jointes n°4
* @property string|null $file5 Pièce jointes n°5
* @property TicketStatusEnum $status
* @property int $order Ticket order.
* @property bool $is_internal
* @property string|null $gitlab_issue URL de l'issue Gitlab
* @property \Illuminate\Support\Carbon|null $date_mep Date MEP souhaitée
* @property int $last_activity Last activity on the ticket.
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read \App\Models\User|null $assignedUser
* @property-read Estimation|null $estimation
* @property-read string $full_name
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Label> $labels
* @property-read \Illuminate\Database\Eloquent\Collection<int, Ticket> $linkedTickets
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Message> $messages
* @property-read \App\Models\User|null $projectManager
* @property-read \App\Models\Site|null $site
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $subscriptions
* @property-read TicketType|null $ticketType
* @property-read \App\Models\User|null $user
* @method static \Illuminate\Database\Eloquent\Builder<static>|Ticket onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Ticket withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Ticket withoutTrashed()
* @mixin \Eloquent
*/
class Ticket extends Model
{
use HasFactory;
use SoftDeletes;
/**
* The attributes that are fillable.
@ -74,10 +32,7 @@ class Ticket extends Model
'site_id',
'type',
'status',
'order',
'date_mep',
'text',
'title',
'is_internal',
'last_activity',
];
@ -88,10 +43,9 @@ class Ticket extends Model
* @var array
*/
protected $casts = [
'is_internal' => 'bool',
'type' => TicketTypeEnum::class,
'status' => TicketStatusEnum::class,
'date_mep' => 'date',
'is_internal' => 'bool',
'type' => TicketTypeEnum::class,
'status' => TicketStatusEnum::class,
'last_activity' => 'timestamp',
];
@ -103,7 +57,6 @@ class Ticket extends Model
protected static function booted()
{
static::creating(function (Ticket $ticket) {
self::setOrder($ticket);
// If bug force the ticket status on new. All the others are estimateTodo.
if ($ticket->type->equals(TicketTypeEnum::bug)) {
$ticket->status = TicketStatusEnum::new;
@ -112,54 +65,28 @@ class Ticket extends Model
}
});
static::updating(function (Ticket $ticket) {
if (
!$ticket->original['status'] !== $ticket->status->value && $ticket->original['order'] === $ticket->order
) {
$user = auth()->user();
if (!$ticket->original['status'] !== $ticket->status->value) {
event(new TicketStatusChanged(
$ticket,
TicketStatusEnum::make($ticket->original['status']),
$user,
auth()->user(),
null,
false
));
$message = $user ? sprintf('<b>%s</b> a modifié le statut en <b>%s</b>', $user->name, $ticket->status->label()) : sprintf('Le statut a été modifié en <b>%s</b>', $ticket->status->label());
event(new NotifySubscriptions($ticket, $message));
event(new NotifyAdmin($ticket, $message));
}
if ($ticket->status->equals(TicketStatusEnum::archived)) {
self::setOrder($ticket, true);
event(new NotifySubscriptions($ticket, sprintf(
'<b>%s</b> a modifié le statut en <b>%s</b>',
auth()->user()->name,
$ticket->status->label()
)));
event(new NotifyAdmin($ticket, sprintf(
'<b>%s</b> a modifié le statut en <b>%s</b>',
auth()->user()->name,
$ticket->status->label()
)));
}
});
}
/**
* Set order after the last element of the list.
*
* @param self $model
* @param boolean $isArchived
* @return void
*/
private static function setOrder(self $model, bool $isArchived = false): void
{
$maxOrder = \intval(self::query()->where('status', '!=', TicketStatusEnum::archived)->max('order'));
if ($isArchived) {
$request = Ticket::query()
->where('site_id', $model->site_id)
->whereNot('status', TicketStatusEnum::archived)
->whereNot('id', $model->id)
->whereBetween('order', [$model->order, $maxOrder]);
$request->decrement('order', 1);
$model->order = 0;
return;
}
$model->order = $maxOrder + 1;
}
// * RELATIONSHIPS
/**
@ -204,17 +131,6 @@ class Ticket extends Model
return $this->belongsTo(User::class, 'assigned_to');
}
/**
* BelongsTo User relation
* The user that is in charge of the site linked to this ticket
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function projectManager()
{
return $this->belongsTo(User::class, 'project_manager_id');
}
/**
* HasMany Message relation
*
@ -256,16 +172,6 @@ class Ticket extends Model
return $this->belongsTo(TicketType::class, 'type', 'type');
}
/**
* BelongsToMany Linked Ticket
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function linkedTickets()
{
return $this->belongsToMany(Ticket::class, 'linked_tickets', 'ticket_id', 'linked_ticket_id');
}
// * METHODS
/**
@ -368,20 +274,4 @@ class Ticket extends Model
Str::limit(\preg_replace('/[[:cntrl:]]+/', '', $ticket->text), 35),
);
}
/**
* Get an array of all files columns of the ticket.
*
* @return array
*/
public function getFiles(): array
{
return [
$this->file,
$this->file2,
$this->file3,
$this->file4,
$this->file5,
];
}
}

View file

@ -9,13 +9,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property TicketTypeEnum $type TicketType Enum
* @property string $template Le template du type de ticket
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, Ticket> $tickets
* @mixin \Eloquent
* @property TicketTypeEnum $type
* @property string $template
*/
class TicketType extends Model
{

View file

@ -15,27 +15,12 @@ use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @property int $id
* @property string $name
* @property string $email
* @property string $password
* @property string $remember_token
* @property RoleEnum $role
* @property string|null $remember_token
* @property int|null $gitlab_id ID du user Gitlab
* @property int|null $favourite_site_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read Collection<int, \App\Models\Message> $messages
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
* @property-read Collection<int, \App\Models\Site> $sites
* @property-read Collection<int, \App\Models\Ticket> $subscriptions
* @property-read Collection<int, \App\Models\Ticket> $tickets
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|User onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|User withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User withoutTrashed()
* @mixin \Eloquent
* @property Collection $sites
*/
class User extends Authenticatable
{
@ -60,9 +45,7 @@ class User extends Authenticatable
'email',
'password',
'role',
'sites',
'gitlab_id',
'favourite_site_id'
'sites'
];
/**
@ -165,8 +148,8 @@ class User extends Authenticatable
*/
public static function adminUsers(): Collection
{
return Cache::remember('AdminUsers', 86400, function () {
return self::whereIn('role', [RoleEnum::admin, RoleEnum::developper])->orderBy('name')->get();
return Cache::rememberForever('AdminUsers', function () {
return self::whereIn('role', [RoleEnum::admin, RoleEnum::developper])->get();
});
}

View file

@ -1,60 +0,0 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Carbon;
class ExportTicketsStateNotification extends Notification implements ShouldQueue
{
use Queueable;
/**
* The CSV content to be sent.
*
* @var string
*/
private string $csvContent;
/**
* Create a new notification instance.
*
* @param string $csvContent The content of the CSV file to be sent.
*/
public function __construct(string $csvContent)
{
$this->csvContent = $csvContent;
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(): array
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail(): \Illuminate\Notifications\Messages\MailMessage
{
return (new MailMessage())
->subject(trans("Export de l'état des lieux des tickets"))
->line(trans("Vous trouverez ci-joint le fichier CSV contenant l'état des tickets."))
->attachData(
$this->csvContent,
sprintf('tickets_state_%s.csv', Carbon::now()->format('Y-m-d_H-i-s')),
[
'mime' => 'text/csv',
]
);
}
}

View file

@ -67,17 +67,6 @@ class TicketPolicy
return $user->isAdmin();
}
/**
* Determine if the user can link ticket .
*
* @param \App\Models\User $user
* @return boolean
*/
public function linkTicket(User $user)
{
return $user->isAdmin();
}
/**
* Determine if the user can change the internal status of a given ticket.
*
@ -283,25 +272,8 @@ class TicketPolicy
public function createGitlabIssue(User $user, Ticket $ticket)
{
return $user->role->equals(RoleEnum::developper)
&& (
$ticket->type->equals(TicketTypeEnum::bug)
|| $ticket->type->equals(TicketTypeEnum::evolution)
|| $ticket->type->equals(TicketTypeEnum::newsletter)
|| $ticket->type->equals(TicketTypeEnum::mailing)
)
&& ($ticket->type->equals(TicketTypeEnum::bug) || $ticket->type->equals(TicketTypeEnum::evolution))
&& !\is_null($ticket->site->gitlab_project_id)
&& \is_null($ticket->gitlab_issue);
}
/**
* Determine if the user can changeOrder
*
* @param User $user
* @return boolean
* @throws Error The error.
*/
public function changeOrder(User $user)
{
return $user->role->equals(RoleEnum::developper);
}
}

View file

@ -2,24 +2,12 @@
namespace App\Providers;
use App\Repositories\EloquentSiteRepository;
use App\Repositories\SiteRepository;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Override;
class AppServiceProvider extends ServiceProvider
{
#[Override]
public function register()
{
$this->app->singleton(
SiteRepository::class,
fn() => new EloquentSiteRepository()
);
}
/**
* Bootstrap any application services.
*
@ -36,7 +24,7 @@ class AppServiceProvider extends ServiceProvider
return request()->getClientIp();
} else {
return request()->server('HTTP_X_FORWARDED_FOR') ??
request()->getClientIp();
request()->getClientIp();
}
});

View file

@ -2,6 +2,14 @@
namespace App\Providers;
use App\Events\NotifyAdmin;
use App\Events\NotifySubscriptions;
use App\Events\NotifyTicketCreator;
use App\Events\TicketStatusChanged;
use App\Listeners\AddStatusMessages;
use App\Listeners\SendNotificationMailToAdmin;
use App\Listeners\SendNotificationMailToTicketCreator;
use App\Listeners\SendSubscriptionsNotificationsMail;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
@ -15,6 +23,10 @@ class EventServiceProvider extends ServiceProvider
*/
protected $listen = [
Registered::class => [SendEmailVerificationNotification::class],
TicketStatusChanged::class => [AddStatusMessages::class],
NotifySubscriptions::class => [SendSubscriptionsNotificationsMail::class],
NotifyTicketCreator::class => [SendNotificationMailToTicketCreator::class],
NotifyAdmin::class => [SendNotificationMailToAdmin::class],
];
/**
@ -24,6 +36,6 @@ class EventServiceProvider extends ServiceProvider
*/
public function shouldDiscoverEvents(): bool
{
return true;
return false;
}
}

View file

@ -6,10 +6,12 @@ use App\Models\Brand;
use App\Models\Company;
use App\Models\Credit\Account;
use App\Models\Credit\CreditLog;
use App\Models\Dutyhour;
use App\Models\Information;
use App\Models\Invoice;
use App\Models\Label;
use App\Models\Package;
use App\Models\Project;
use App\Models\Site;
use App\Models\Ticket;
use App\Models\Ticket\TicketType;
@ -87,12 +89,15 @@ class RouteServiceProvider extends ServiceProvider
});
Route::model('brand', Brand::class);
Route::model('label', Label::class);
Route::model('marker', Marker::class);
Route::model('package', Package::class);
Route::model('dutyhour', Dutyhour::class);
Route::model('information', Information::class);
Route::model('company', Company::class);
Route::model('invoice', Invoice::class);
Route::model('user', User::class);
Route::model('site', Site::class);
Route::model('project', Project::class);
Route::model('ticket_type', TicketType::class);
Route::bind('ticket', function ($value) {
return Ticket::where('id', $value)->has('site')->firstOrFail();

View file

@ -19,7 +19,7 @@ class TranslationServiceProvider extends ServiceProvider
{
foreach ($this->collectLocalesStrings() as $locale) {
// * Supported locales
Cache::remember(sprintf('translations.%s', $locale), 84600, function () use ($locale) {
Cache::rememberForever(sprintf('translations.%s', $locale), function () use ($locale) {
$translations = [
'php' => $this->phpTranslations($locale),
'json' => $this->jsonTranslations($locale),

View file

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Site;
use Override;
class EloquentSiteRepository implements SiteRepository
{
#[Override]
public function findByName(string $name): ?Site
{
return Site::query()
->where('name', $name)
->first();
}
}

View file

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Repositories;
use App\Models\Site;
interface SiteRepository
{
public function findByName(string $name): ?Site;
}

View file

@ -1,76 +0,0 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\ValidationRule;
class Hex implements ValidationRule
{
/**
* @var boolean
*/
protected $forceFull;
/**
* @var boolean
*/
protected $allowAlpha;
/**
* __construct method
*
* @param boolean $forceFull
* @param boolean $allowAlpha
*
* @return void
*/
public function __construct(bool $forceFull = false, bool $allowAlpha = false)
{
$this->forceFull = $forceFull;
$this->allowAlpha = $allowAlpha;
}
/**
* Run the validation rule.
*
* @param string $attribute
* @param mixed $value
* @param \Closure(string):\Illuminate\Translation\PotentiallyTranslatedString $fail
* @return void
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
public function validate(string $attribute, mixed $value, \Closure $fail): void
{
$pattern = '/^#([a-fA-F0-9]{6}';
if (!$this->forceFull) {
$pattern .= '|[a-fA-F0-9]{3}';
}
if ($this->allowAlpha) {
$pattern .= '|[a-fA-F0-9]{8}';
if (!$this->forceFull) {
$pattern .= '|[a-fA-F0-9]{4}';
}
}
$pattern .= ')$/';
if (!(bool)preg_match($pattern, $value)) {
$fail(trans($this->message()));
}
}
/**
* Get the validation error message.
*
* @return string
*/
public function message(): string
{
return 'The hex code is not valid';
}
}

Some files were not shown because too many files have changed in this diff Show more