Compare commits

..

1 commit

Author SHA1 Message Date
Alexis Gousseau
2609c5e540 fix: 🎨 resolve phpcs and phpstan errors 2025-04-23 12:24:31 +02:00
236 changed files with 3723 additions and 10808 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=

4
.gitignore vendored
View file

@ -22,7 +22,3 @@ 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.3: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

@ -2,16 +2,11 @@
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
Le token de groupe pour l'intégration est valable 1 an max
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
- App\Models\Message:booted(): investigate why this make errors on prod,

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,7 @@ class CreateUser extends Command
/**
* The console command description.
*
* @var string
* @var string|null
*/
protected $description = 'Create a user';
@ -34,21 +34,21 @@ class CreateUser extends Command
/**
* The email of the new user.
*
* @var string
* @var string|null
*/
protected $email;
/**
* The password of the new user.
*
* @var string
* @var string|null
*/
protected $password;
/**
* The role of the new user.
*
* @var integer
* @var \App\Enums\Users\RoleEnum|null
*/
protected $role;
@ -59,7 +59,7 @@ class CreateUser extends Command
*/
public function handle()
{
$this->name = null;
$this->name = "";
$this->addName();
$this->email = null;
@ -72,10 +72,10 @@ class CreateUser extends Command
$this->addRole();
$user = new User([
'name' => $this->name,
'email' => $this->email,
'name' => $this->name,
'email' => $this->email,
'password' => $this->password,
'role' => $this->role
'role' => $this->role
]);
$user->saveOrFail();
@ -90,7 +90,7 @@ class CreateUser extends Command
*/
private function addName(): void
{
while (is_null($this->name)) {
while (empty($this->name)) {
try {
$tmp = $this->ask('Type the wanted user name', 'Admin');
$validator = Validator::make(['name' => $tmp], [
@ -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

@ -12,10 +12,12 @@ enum AccountTypeEnum: int
case seo = 1;
/**
* Optionnal labels definition
* Optionnal labels definition.
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::development->name => "Jours de développement",
self::seo->name => "Jours de seo",
self::seo->name => "Jours de seo",
];
}

View file

@ -12,10 +12,12 @@ enum BalanceOperationEnum: int
case debit = 1;
/**
* Optionnal labels definition
* Optionnal labels definition.
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::credit->name => "Créditer un compte",
self::debit->name => "Débiter un compte",
self::debit->name => "Débiter un compte",
];
}

View file

@ -13,11 +13,13 @@ enum EstimationStatusEnum: int
case refused = 2;
/**
* Optionnal labels definition
* Optionnal labels definition.
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::pending->name => "En attente",
self::pending->name => "En attente",
self::accepted->name => "Acceptée",
self::refused->name => "Refusée",
self::refused->name => "Refusée",
];
}

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

@ -18,21 +18,21 @@ enum InfoColorsEnum: int
case dark = 7;
/**
* Optionnal labels definition
* Optionnal labels definition.
*/
private const CLASSES = [
self::primary->name => "primary",
self::primary->name => "primary",
self::secondary->name => "secondary",
self::success->name => "success",
self::danger->name => "danger",
self::warning->name => "warning",
self::info->name => "info",
self::light->name => "light",
self::dark->name => "dark",
self::success->name => "success",
self::danger->name => "danger",
self::warning->name => "warning",
self::info->name => "info",
self::light->name => "light",
self::dark->name => "dark",
];
/**
* Get Class
* Get Class.
*
* @return string
*/
@ -42,7 +42,7 @@ enum InfoColorsEnum: int
}
/**
* Get enum classes
* Get enum classes.
*
* @return array<string>
*/

View file

@ -8,22 +8,20 @@ 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.
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::opened->name => "Ouvert (nouveaux ou en cours)",
self::refused->name => "Refusé (par le client ou aucune réponse de sa part)",
self::opened->name => "Ouvert (nouveaux ou en cours)",
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

@ -14,41 +14,43 @@ enum TaskTypeEnum: int
case arbitrary = 4;
/**
* Optionnal labels definition
* Optionnal labels definition.
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::web->name => "Web",
self::seo->name => "SEO",
self::package->name => "Forfait",
self::web->name => "Web",
self::seo->name => "SEO",
self::package->name => "Forfait",
self::arbitrary->name => "Libre",
];
/**
* Custom added prices definition
* Custom added prices definition.
*/
private const PRICES = [
self::web->name => 600,
self::seo->name => 690,
self::web->name => 600,
self::seo->name => 690,
// * Linked to packages table
self::package->name => 0,
self::package->name => 0,
// * Arbitrary number
self::arbitrary->name => 0,
];
/**
* Get Price
* Get Price.
*
* @return string
* @return integer
*/
public function price(): string
public function price(): int
{
return self::PRICES[$this->name];
}
/**
* Get enum prices
* Get enum prices.
*
* @return array<string>
* @return array<string, int>
*/
public static function toPrices(): array
{

View file

@ -12,10 +12,12 @@ enum MessageTypeEnum: int
case public = 2;
/**
* Optionnal labels definition
* Optionnal labels definition.
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::private->name => "Privé",
self::public->name => "Public",
self::public->name => "Public",
];
}

View file

@ -4,7 +4,7 @@ namespace App\Enums\Tickets;
use App\Traits\Enums\BaseEnum;
enum TicketFileTypeEnum: int
enum TicketFileTypeEnum: string
{
use BaseEnum;
@ -16,10 +16,11 @@ enum TicketFileTypeEnum: int
case docx = '.docx';
case xlsx = '.xlsx';
case csv = '.csv';
case svg = '.svg';
/**
* Optionnal labels definition
* Optionnal labels definition.
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::png->name => "Fichier png",
@ -30,11 +31,10 @@ enum TicketFileTypeEnum: int
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
* Custom added mimes type definition.
*/
private const MIMETYPE = [
self::png->name => 'image/png',
@ -45,11 +45,10 @@ enum TicketFileTypeEnum: int
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
* Get Class.
*
* @return string
*/
@ -59,7 +58,7 @@ enum TicketFileTypeEnum: int
}
/**
* Get enum classes
* Get enum classes.
*
* @return array<string>
*/

View file

@ -17,42 +17,41 @@ enum TicketStatusEnum: int
case recetteValidated = 7;
case production = 8;
case archived = 9;
case inDev = 10;
/**
* Optionnal labels definition
* Optionnal labels definition.
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::new->name => "Nouveau",
self::estimateTodo->name => "Étude chef(s) de projet",
self::takenInAccount->name => "À planifier",
self::waiting->name => "En attente de réponse",
self::new->name => "Nouveau",
self::estimateTodo->name => "Étude chef(s) de projet",
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",
self::recette->name => "En recettage",
self::recetteValidated->name => "Recettage validé",
self::production->name => "En production",
self::archived->name => "Archivé",
];
/**
* Custom added classes definition
* Custom added classes definition.
*/
private const CLASSES = [
self::new->name => 'text-danger',
self::estimateTodo->name => 'text-info',
self::takenInAccount->name => 'text-info',
self::waiting->name => 'text-white bg-danger',
self::new->name => 'text-danger',
self::estimateTodo->name => 'text-info',
self::takenInAccount->name => 'text-info',
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',
self::recette->name => 'text-primary',
self::recetteValidated->name => 'text-success',
self::production->name => 'text-success',
self::archived->name => 'text-secondary',
];
/**
* Get Class
* Get Class.
*
* @return string
*/
@ -62,7 +61,7 @@ enum TicketStatusEnum: int
}
/**
* Get enum classes
* Get enum classes.
*
* @return array<string>
*/

View file

@ -14,35 +14,31 @@ enum TicketTypeEnum: int
case newsletter = 4;
case qrcode = 5;
case advice = 6;
case complaints = 7;
case mailing = 8;
/**
* Optionnal labels definition
* Optionnal labels definition.
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::bug->name => "Bug",
self::evolution->name => "Évolution",
self::br->name => "Demande BR",
self::bug->name => "Bug",
self::evolution->name => "Évolution",
self::br->name => "Demande BR",
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",
self::qrcode->name => "Demande de QR Code",
self::advice->name => "Conseil - Accompagnement",
];
/**
* Custom added classes definition
*/
private const CLASSES = [
self::bug->name => "bg-danger",
self::evolution->name => "bg-success",
self::br->name => "bg-primary",
self::bug->name => "bg-danger",
self::evolution->name => "bg-success",
self::br->name => "bg-primary",
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",
self::qrcode->name => "bg-secondary",
self::advice->name => "bg-info",
];
/**

View file

@ -0,0 +1,26 @@
<?php
namespace App\Enums\Users;
use App\Traits\Enums\BaseEnum;
enum GitlabUsersEnum: int
{
use BaseEnum;
/**
* Each user is associated to his ID on gitlab.
*
* 24 is Nicolas Demangeat
*/
case default = 24;
/**
* Optionnal labels definition.
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::default->name => "Default",
];
}

View file

@ -13,11 +13,13 @@ enum RoleEnum: int
case client = 2;
/**
* Optionnal labels definition
* Optionnal labels definition.
*
* @phpstan-ignore-next-line
*/
private const LABELS = [
self::developper->name => "Developpeur",
self::admin->name => "Administrateur",
self::client->name => "Client",
self::admin->name => "Administrateur",
self::client->name => "Client",
];
}

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

@ -15,7 +15,7 @@ class TicketStatusChanged
use InteractsWithSockets;
use SerializesModels;
/** @var \App\Models\Site */
/** @var \App\Models\Ticket */
public $ticket;
/** @var \App\Models\User */

View file

@ -36,7 +36,7 @@ class Handler extends ExceptionHandler
*
* @return void
*/
public function register()
public function register(): void
{
$this->reportable(function (\Throwable $e) {
if (app()->bound('sentry')) {

View file

@ -10,7 +10,7 @@ class ForgotPasswordController extends Controller
use SendsPasswordResetEmails;
/**
* ForgotPasswordController
* ForgotPasswordController.
*
* @return void
*/

View file

@ -23,11 +23,11 @@ class ResetPasswordController extends Controller
*
* @return array
*/
protected function rules()
protected function rules(): array
{
return [
'token' => 'required',
'email' => 'required|email',
'token' => 'required',
'email' => 'required|email',
'password' => [
'required', 'confirmed', Password::min(8)
->mixedCase()

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

@ -7,6 +7,7 @@ use App\Http\Requests\Bo\CreditAccounts\StoreCreditAccountRequest;
use App\Models\Brand;
use App\Models\Credit\Account;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
@ -21,13 +22,11 @@ class AccountController 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()) {
$brandModels = $user->brands()->pluck('id');
} else {
$brandModels = $request->brand ?? Brand::all()->pluck('id');
}
$brandModels = (!$user->isAdmin())
? $user->brands()->pluck('id')
: $request->brand ?? Brand::query()->pluck('id');
/** @var \Illuminate\Database\Eloquent\Builder $credit_accountModels */
$credit_accountModels = Account::query()->whereIn('brand_id', $brandModels);
@ -62,14 +61,14 @@ class AccountController extends Controller
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Brand $brand
* @param \App\Models\Account $account
* @param \App\Models\Brand $brand
* @param \App\Models\Credit\Account $account
* @return \Illuminate\Contracts\View\View
*/
public function create(Brand $brand, Account $account): \Illuminate\Contracts\View\View
{
return view('bo.pages.credit_accounts.create', [
'brandModel' => $brand,
'brandModel' => $brand,
'credit_accountModel' => $account
]);
}
@ -85,7 +84,7 @@ class AccountController extends Controller
{
return DB::transaction(function () use ($brand, $request) {
$credit_accountModel = new Account();
$credit_accountModel->brand_id = $brand->id;
$credit_accountModel->brand_id = $brand->getKey();
$credit_accountModel->fill($request->validated());
if ($credit_accountModel->saveOrFail()) {

View file

@ -58,14 +58,14 @@ class CreditLogController extends Controller
/**
* Show the form for creating a new resource.
*
* @param \App\Models\Brand $brand
* @param \App\Models\Account $account
* @param \App\Models\Brand $brand
* @param \App\Models\Credit\Account $account
* @return \Illuminate\Contracts\View\View
*/
public function create(Brand $brand, Account $account): \Illuminate\Contracts\View\View
{
return view('bo.pages.credit_logs.create', [
'brandModel' => $brand,
'brandModel' => $brand,
'credit_accountModel' => $account
]);
}
@ -74,7 +74,7 @@ class CreditLogController extends Controller
* Store a newly created resource in storage.
*
* @param \App\Models\Brand $brand
* @param \App\Models\Account $account
* @param \App\Models\Credit\Account $account
* @param \App\Http\Requests\Bo\CreditLogs\StoreCreditLogRequest $request
* @return \Illuminate\Http\RedirectResponse
*/

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()->orderByDesc('created_at');
$status = intval($request->status);
$invoiceModels = $invoiceModels->when($status, function ($query) use ($status) {
$query->where('status', $status);
});
/** @var string $search Search field */
$search = $request->search;
@ -90,7 +75,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();
@ -191,8 +176,9 @@ class InvoiceController extends Controller
* @param \App\Models\Invoice $invoice
* @return \Illuminate\Http\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function download(Invoice $invoice)
{
public function download(
Invoice $invoice
): \Illuminate\Http\RedirectResponse|\Symfony\Component\HttpFoundation\Response {
if (Gate::allows('isAdmin')) {
return $invoice->generateInvoice()
->stream(Str::slug(trans('Devis :appname :date - ref :ref', [
@ -214,7 +200,7 @@ class InvoiceController extends Controller
* Get a paginate resource.
*
* @param \Illuminate\Http\Request $request
* @return Illuminate\Http\JsonResponse
* @return \Illuminate\Http\JsonResponse
*/
public function jsonPaginate(Request $request): \Illuminate\Http\JsonResponse
{

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
*/
public function show(Project $project): \Illuminate\Contracts\View\View
{
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

@ -28,11 +28,11 @@ class SiteController extends Controller
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);
});
}
@ -189,7 +189,7 @@ class SiteController extends Controller
* Get a paginate resource.
*
* @param \Illuminate\Http\Request $request
* @return Illuminate\Http\JsonResponse
* @return \Illuminate\Http\JsonResponse
*/
public function jsonPaginate(Request $request): \Illuminate\Http\JsonResponse
{

View file

@ -2,48 +2,45 @@
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\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\UpdateChangeOrderRequest;
use App\Http\Requests\Bo\UpdateMessage;
use Illuminate\Support\Facades\Session;
use App\Http\Requests\Bo\UpdateTicket;
use App\Http\Requests\Bo\LinkTicket;
use App\Lib\Helpers\FileStorageHelper;
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\Arr;
use Illuminate\Support\Facades\Auth;
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');
}
@ -66,7 +63,7 @@ class TicketController extends Controller
$q->where('name', 'LIKE', "%$request->s%");
});
}
/** @var \Illuminate/Pagination/LengthAwarePaginator $tickets */
/** @var \Illuminate\Pagination\LengthAwarePaginator $tickets */
$tickets = $tickets->paginate();
return \response()->json($tickets->getCollection()->map(function (Ticket $ticket) {
return [
@ -132,10 +129,12 @@ class TicketController extends Controller
*
* @param Request $request
* @param Ticket $ticket
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
* @return \Illuminate\Http\JsonResponse|\Illuminate\Contracts\View\View
*/
public function show(Request $request, Ticket $ticket)
{
public function show(
Request $request,
Ticket $ticket
): \Illuminate\Http\JsonResponse|\Illuminate\Contracts\View\View {
if ($request->ajax()) {
return \response()->json([
'label' => $ticket->formatName($ticket),
@ -166,9 +165,9 @@ class TicketController extends Controller
/**
* Show create Form
*
* @return \Illuminate\Http\RedirectResponse
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Contracts\View\View
*/
public function create()
public function create(): \Illuminate\Http\RedirectResponse|\Illuminate\Contracts\View\View
{
/** @var \App\Models\User */
$user = auth()->user();
@ -191,7 +190,7 @@ class TicketController extends Controller
* @param StoreTicket $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreTicket $request)
public function store(StoreTicket $request): \Illuminate\Http\RedirectResponse
{
try {
return DB::transaction(function () use ($request) {
@ -216,7 +215,7 @@ class TicketController extends Controller
}
}
$ticket->user()->associate($user);
$ticket->user()->associate(auth()->user());
$ticket->site()->associate($site);
$ticket->saveOrFail();
@ -226,11 +225,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é !'));
@ -244,13 +243,13 @@ class TicketController extends Controller
}
/**
* Show update form
* Show update form.
*
* @param Ticket $ticket
* @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
* @return \Illuminate\Contracts\View\View
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/
public function edit(Ticket $ticket)
public function edit(Ticket $ticket): \Illuminate\Contracts\View\View
{
/** @var \App\Models\User */
$user = auth()->user();
@ -263,13 +262,13 @@ class TicketController extends Controller
}
/**
* Update Model
* Update Model.
*
* @param UpdateTicket $request
* @param Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateTicket $request, Ticket $ticket)
public function update(UpdateTicket $request, Ticket $ticket): \Illuminate\Http\RedirectResponse
{
$ticket->fill($request->validated());
@ -308,18 +307,14 @@ class TicketController extends Controller
}
/**
* Change ticket Status
* Change ticket Status.
*
* @param ChangeTicketStatus $request
* @param Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function changeStatus(ChangeTicketStatus $request, Ticket $ticket)
public function changeStatus(ChangeTicketStatus $request, Ticket $ticket): \Illuminate\Http\RedirectResponse
{
if ($ticket->gitlab_issue) {
\dispatch(new UpdateIssueLabels($ticket, $request->status));
}
$ticket->fill($request->validated());
$ticket->saveOrFail();
@ -328,32 +323,33 @@ class TicketController extends Controller
}
/**
* Add a new Message to the Ticket
* Add a new Message to the Ticket.
*
* @param CreateMessage $request
* @param Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function createMessage(CreateMessage $request, Ticket $ticket)
public function createMessage(CreateMessage $request, Ticket $ticket): \Illuminate\Http\RedirectResponse
{
try {
return DB::transaction(function () use ($request, $ticket) {
/** @var \App\Models\User $user */
$user = Auth::user();
/** @var \Illuminate\Http\UploadedFile|null */
$filesMessage = $request->file_messages;
$message = new Message($request->validated());
$message->user()->associate(auth()->user());
$message->user()->associate($user);
$message->ticket()->associate($ticket);
/** @var \App\Models\User */
$user = auth()->user();
// Subscribe to ticket.
if ($request->subscribe) {
$user->subscriptions()->syncWithoutDetaching([$ticket->id]);
} else {
if (!$user->subscriptions()->find($ticket->id)) {
$user->subscriptions()->attach($ticket->id);
}
} elseif ($user->subscriptions()->find($ticket->id)) {
$user->subscriptions()->detach($ticket->id);
}
// Store files if any.
if (isset($filesMessage) and $filesMessage) {
foreach ($filesMessage as $key => $singleFile) {
if ($key === 0) {
@ -371,7 +367,7 @@ class TicketController extends Controller
return back()
->with('success', trans('Votre message à été ajouté'));
}, 5);
});
} catch (\ErrorException $e) {
report($e);
Session::flash('error', trans('Une erreur est survenue'));
@ -381,7 +377,7 @@ class TicketController extends Controller
}
/**
* Update Message
* Update Message.
*
* @param UpdateMessage $request
* @param Ticket $ticket
@ -389,8 +385,11 @@ class TicketController extends Controller
* @return \Illuminate\Http\RedirectResponse
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
*/
public function updateMessage(UpdateMessage $request, Ticket $ticket, Message $message)
{
public function updateMessage(
UpdateMessage $request,
Ticket $ticket,
Message $message
): \Illuminate\Http\RedirectResponse {
$message->message = $request->message;
$message->saveOrFail();
@ -399,12 +398,12 @@ class TicketController extends Controller
}
/**
* Change ticket status to archived
* Change ticket status to archived.
*
* @param Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function setToArchive(Ticket $ticket)
public function setToArchive(Ticket $ticket): \Illuminate\Http\RedirectResponse
{
$ticket->status = TicketStatusEnum::archived;
$ticket->saveOrFail();
@ -413,15 +412,15 @@ class TicketController extends Controller
}
/**
* Subscribe to ticket
* Subscribe to ticket.
*
* @param Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function subscribe(Ticket $ticket)
public function subscribe(Ticket $ticket): \Illuminate\Http\RedirectResponse
{
/** @var \App\Models\User */
$user = auth()->user();
/** @var \App\Models\User $user */
$user = Auth::user();
$user->subscriptions()->toggle($ticket->id);
if ($user->subscriptions()->where('ticket_id', $ticket->id)->exists()) {
return back()
@ -431,15 +430,15 @@ class TicketController extends Controller
}
/**
* Reopen ticket
* Reopen ticket.
*
* @param Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function reopen(Ticket $ticket)
public function reopen(Ticket $ticket): \Illuminate\Http\RedirectResponse
{
$message = trans(':ticketName à réouvert le ticket n°:ticketNumber', [
'ticketName' => $ticket->user->name,
'ticketName' => $ticket->user->name,
'ticketNumber' => $ticket->id,
]);
/** @var \App\Enums\Tickets\TicketStatusEnum */
@ -449,27 +448,28 @@ class TicketController extends Controller
event(new TicketStatusChanged($ticket, $oldStatus, null, $message, false, false));
event(new NotifySubscriptions($ticket, $message, collect([$ticket->user])));
/** @var \App\Models\User */
$user = auth()->user();
/** @var \App\Models\User $user */
$user = Auth::user();
$user->subscriptions()->toggle($ticket->id);
return back()->with('warning', trans('Votre ticket à été réouvert.'));
}
/**
* Assign ticket to user ticket
* Assign ticket to user ticket.
*
* @param AssignTicketToUserRequest $request
* @param Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
*/
public function assignTo(AssignTicketToUserRequest $request, Ticket $ticket)
public function assignTo(AssignTicketToUserRequest $request, Ticket $ticket): \Illuminate\Http\RedirectResponse
{
/** @var \App\Models\User */
$user = auth()->user();
/** @var \App\Models\User $user */
$user = Auth::user();
/** @var \App\Models\User|null */
if ($assignedUser = User::whereId($request->assign)->first()) {
/** @var \App\Models\User|null $assignedUser */
$assignedUser = User::whereId($request->assign)->first();
if ($assignedUser) {
$message = trans(':admin a assigné le ticket n°:ticketNumber à :assignedAdmin', [
'admin' => $user->name,
'ticketNumber' => $ticket->id,
@ -495,8 +495,6 @@ class TicketController extends Controller
event(new TicketStatusChanged($ticket, $oldStatus, null, $message, true, false));
event(new NotifySubscriptions($ticket, $message, collect([$ticket->user])));
/** @var \App\Models\User */
$user = auth()->user();
$user->subscriptions()->toggle($ticket->id);
if ($assignedUser) {
@ -552,14 +550,14 @@ class TicketController extends Controller
// * PRIVATES
/**
* Filter Tickets request constructor
* Filter Tickets request constructor.
*
* @param Builder $tickets
* @param Request $request
* @return Builder
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
* @param \Illuminate\Database\Eloquent\Builder $tickets
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Database\Eloquent\Builder
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded
*/
private function filterTickets(Builder $tickets, Request $request): Builder
private function filterTickets(Builder $tickets, Request $request): \Illuminate\Database\Eloquent\Builder
{
//phpcs:enable
if ($request->rst) {
@ -666,13 +664,13 @@ class TicketController extends Controller
}
/**
* Search ticket sql request constructor
* Search ticket sql request constructor.
*
* @param Builder $tickets
* @param Request $request
* @return Builder
* @param \Illuminate\Database\Eloquent\Builder $tickets
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Database\Eloquent\Builder
*/
private function searchTicket(Builder $tickets, Request $request): Builder
private function searchTicket(Builder $tickets, Request $request): \Illuminate\Database\Eloquent\Builder
{
if ($request->exists('search')) {
if (empty($request->search)) {
@ -688,29 +686,27 @@ class TicketController extends Controller
return $this->searchQuery($tickets, Session::get('tickets.search'), function ($q) {
$words = explode(' ', Session::get('tickets.search'));
if (\is_array($words)) {
foreach ($words as $word) {
if (!strlen($word)) {
continue;
}
$s = collect(TicketStatusEnum::cases())->filter(function (TicketStatusEnum $t) use ($word) {
return Str::slug($t->label()) === Str::slug($word);
})->map(function (TicketStatusEnum $ticket) {
return $ticket->value;
foreach ($words as $word) {
if (!strlen($word)) {
continue;
}
$s = collect(TicketStatusEnum::cases())->filter(function (TicketStatusEnum $t) use ($word) {
return Str::slug($t->label()) === Str::slug($word);
})->map(function (TicketStatusEnum $ticket) {
return $ticket->value;
});
$t = collect(TicketTypeEnum::cases())->filter(function (TicketTypeEnum $s) use ($word) {
return Str::slug($s->label()) === Str::slug($word);
})->map(function (TicketTypeEnum $ticket) {
return $ticket->value;
});
$q->orWhere('id', $word)
->orWhereIn('status', $s)
->orWhereIn('type', $t)
->orWhereHas('user', function ($q) use ($word) {
$q->where('name', 'LIKE', "%$word%");
});
$t = collect(TicketTypeEnum::cases())->filter(function (TicketTypeEnum $s) use ($word) {
return Str::slug($s->label()) === Str::slug($word);
})->map(function (TicketTypeEnum $ticket) {
return $ticket->value;
});
$q->orWhere('id', $word)
->orWhereIn('status', $s)
->orWhereIn('type', $t)
->orWhereHas('user', function ($q) use ($word) {
$q->where('name', 'LIKE', "%$word%");
});
} //end foreach
} //end if
} //end foreach
}, 'text', 'title', 'file', 'file2', 'file3', 'file4', 'file5');
}
@ -718,7 +714,7 @@ class TicketController extends Controller
* Get a paginate resource.
*
* @param \Illuminate\Http\Request $request
* @return Illuminate\Http\JsonResponse
* @return \Illuminate\Http\JsonResponse
*/
public function jsonPaginate(Request $request): \Illuminate\Http\JsonResponse
{
@ -749,29 +745,22 @@ class TicketController extends Controller
*
* @param \App\Models\Ticket $ticket
* @return \Illuminate\Http\RedirectResponse
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
*/
public function createGitlabIssue(Ticket $ticket)
public function createGitlabIssue(Ticket $ticket): \Illuminate\Http\RedirectResponse
{
$userGitlabId = auth()->user()->gitlab_id;
//phpcs:enable
$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',
$description = \sprintf("%s<br>", $ticket->text);
$cLabel = match ($ticket->type) {
TicketTypeEnum::bug => 'c-bug',
TicketTypeEnum::evolution, TicketTypeEnum::newsletter => 'c-feature',
};
$assigneeId = match ($ticket->type) {
TicketTypeEnum::bug => $userGitlabId,
TicketTypeEnum::bug => GitlabUsersEnum::default->value,
TicketTypeEnum::evolution,
TicketTypeEnum::mailing,
TicketTypeEnum::newsletter => $ticket->site->projectManager?->gitlab_id
TicketTypeEnum::newsletter => $ticket->site->projectManager?->gitlab_id ?? GitlabUsersEnum::default->value
};
try {
@ -781,8 +770,7 @@ class TicketController extends Controller
$description .= $this->formatGitlabFiles($ticket, $uploadedFile, $key + 1);
}
}
$data = [
$issue = GitLab::issues()->create($gitlabRepoId, [
'title' => sprintf(
'ts-%s : %s',
$ticket->id,
@ -793,13 +781,8 @@ class TicketController extends Controller
'c-support',
$cLabel
]),
];
if ($assigneeId) {
$data['assignee_id'] = $assigneeId;
}
$issue = GitLab::issues()->create($gitlabRepoId, $data);
'assignee_id' => $assigneeId,
]);
} catch (\Exception $e) {
\report($e);
return back()->with('error', \trans("Une erreur est survenue"));
@ -818,16 +801,16 @@ class TicketController extends Controller
}
/**
* Format files for Gitlab issues
* Format files for Gitlab issues.
*
* @param \App\Models\Ticket $ticket
* @param array $uploadedFile
* @param integer $fileNb
* @return string
*/
private function formatGitlabFiles(Ticket $ticket, array $uploadedFile, int $fileNb)
private function formatGitlabFiles(Ticket $ticket, array $uploadedFile, int $fileNb): string
{
if (Str::endsWith($ticket->file, ['jpg', 'png', 'jpeg', 'pptx', 'docx', 'xslx', 'csv', 'svg'])) {
if (Str::endsWith($ticket->file, ['jpg', 'png', 'jpeg', 'pptx', 'docx', 'xslx', 'csv'])) {
$file = \sprintf(
"![Fichier joint %d](%s)",
$fileNb,
@ -856,7 +839,8 @@ class TicketController extends Controller
$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);
/** @var \App\Models\Ticket|null $model */
$model = Arr::get($params, $paramNames->count() - 2);
if (!class_exists($className)) {
throw new \RuntimeException("The {$className} class doesn't exist.");
@ -870,7 +854,7 @@ class TicketController extends Controller
throw new \RuntimeException("The {$className} doesn't match second last binded param.");
}
/** @var \Illuminate\Database\Eloquent\Model|null */
/** @var \App\Models\Ticket|null */
$model = $className::query()->where($model->getRouteKeyName(), $model->getRouteKey())->first();
if (!$model) {
@ -878,7 +862,7 @@ class TicketController extends Controller
}
return DB::transaction(function () use ($request, $className, $model) {
/** @var \Illuminate\Support\Collection $models */
/** @var \Illuminate\Support\Collection<int, \App\Models\Ticket> $models */
$models = $className::query()
->orderBy('order', 'ASC')
->where('site_id', $model->site_id)
@ -900,34 +884,4 @@ class TicketController extends Controller
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

@ -45,7 +45,7 @@ class TicketTypeController extends Controller
/**
* Show the form for editing the specified resource.
*
* @param App\Models\Ticket\TicketType $ticketType
* @param \App\Models\Ticket\TicketType $ticketType
* @return \Illuminate\Contracts\View\View
* @ignore phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/

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'));
@ -168,43 +166,17 @@ class UserController extends Controller
}
/**
* Password reset form
* Password reset form.
*
* @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');
return view('bo.pages.users.passwordReset');
}
/**
* 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'));
}
}
/**
* Password reset submit
* Password reset submit.
*
* @return \Illuminate\Http\RedirectResponse
*/
@ -215,18 +187,4 @@ class UserController extends Controller
$request = new Request(['email' => $user->email]);
return app(ForgotPasswordController::class)->sendResetLinkEmail($request);
}
/**
* Get february last day date of the year
*
* @param integer $year
* @return integer 28 or 29
*/
private function getLastDayFebruary(int $year): int
{
if ((is_int($year / 4) && !is_int($year / 100)) || is_int($year / 400)) {
return 29;
}
return 28;
}
}

View file

@ -4,7 +4,6 @@ namespace App\Http\Controllers;
use App\Enums\Tickets\TicketStatusEnum;
use App\Models\Ticket;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
@ -89,7 +88,7 @@ class Controller extends BaseController
string $search,
callable $searchQuery = null,
string ...$fields
): Builder {
): \Illuminate\Database\Eloquent\Builder {
if (count($fields)) {
// Search using words.
$words = explode(' ', $search);
@ -199,7 +198,7 @@ class Controller extends BaseController
* @param array $ignore
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function sortQuery(Builder $query, array $ignore = []): Builder
protected function sortQuery(Builder $query, array $ignore = []): \Illuminate\Database\Eloquent\Builder
{
$rName = request()->route()->getName();
$table = $query->getModel()->getTable();
@ -216,13 +215,12 @@ 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 (
!in_array(request()->sort_col, $ignore)
) {
$query = $query->orderBy(\sprintf("$table.%s", request()->sort_col), request()->sort_way);
}
}
return $query;
}
@ -234,7 +232,7 @@ class Controller extends BaseController
* @param integer|null $pagination
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
protected function customPaginate(Builder $query, $pagination): LengthAwarePaginator
protected function customPaginate(Builder $query, $pagination): \Illuminate\Pagination\LengthAwarePaginator
{
if ($pagination) {
Cache::put('pagination', $pagination);

View file

@ -54,17 +54,17 @@ class Kernel extends HttpKernel
* @var array<string, class-string|string>
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'is-admin' => \App\Http\Middleware\IsAdmin::class,
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'is-admin' => \App\Http\Middleware\IsAdmin::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class,
];
}

View file

@ -34,7 +34,5 @@ class Authenticate extends Middleware
return route('login');
}
abort(401);
return null;
}
}

View file

@ -4,7 +4,6 @@ namespace App\Http\Middleware;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class HtmlSanitizer
{
@ -16,8 +15,11 @@ class HtmlSanitizer
* @param string|array ...$params
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, \Closure $next, string|array ...$params): Response
{
public function handle(
Request $request,
\Closure $next,
string|array ...$params
): \Symfony\Component\HttpFoundation\Response {
$inputs = $request->all();
/** @var callable|null */
$cleanInput = null;

View file

@ -4,7 +4,6 @@ namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
@ -18,7 +17,7 @@ class RedirectIfAuthenticated
* @throws \RuntimeException If guard is unhandled.
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
public function handle(Request $request, \Closure $next, ...$guards): Response
public function handle(Request $request, \Closure $next, ...$guards): \Symfony\Component\HttpFoundation\Response
{
// phpcs:enable phpcs:disable
$guards = empty($guards) ? [null] : $guards;
@ -28,7 +27,7 @@ class RedirectIfAuthenticated
switch ($guard) {
case 'backend':
if (!$request->routeIs('bo.*') && !$request->routeIs('login')) {
continue 2;
continue;
}
return \redirect()->route('bo.dashboard');
default:

View file

@ -7,7 +7,6 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Vite;
use Symfony\Component\HttpFoundation\Response;
class SecureHeaders
{
@ -31,7 +30,7 @@ class SecureHeaders
* @return \Symfony\Component\HttpFoundation\Response
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
public function handle(Request $request, Closure $next): Response
public function handle(Request $request, Closure $next): \Symfony\Component\HttpFoundation\Response
{
// phpcs:enable
View::share('nonce', $nonce = Vite::useCspNonce());
@ -89,12 +88,12 @@ class SecureHeaders
}
/**
* Remove unwanted headers
* Remove unwanted headers.
*
* @param array $headerList
* @return void
*/
private function removeUnwantedHeaders(array $headerList)
private function removeUnwantedHeaders(array $headerList): void
{
foreach ($headerList as $header) {
header_remove($header);

View file

@ -11,7 +11,7 @@ class TrustHosts extends Middleware
*
* @return array<int, string|null>
*/
public function hosts()
public function hosts(): array
{
return [
$this->allSubdomainsOfApplicationUrl(),

View file

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

View file

@ -4,6 +4,7 @@ namespace App\Http\Requests\Bo;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class AssignTicketToUserRequest extends FormRequest
{
@ -12,11 +13,11 @@ class AssignTicketToUserRequest extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
/** @var \App\Models\User */
$user = auth()->user();
return auth()->check() and $user->isAdmin();
/** @var \App\Models\User $user */
$user = Auth::user();
return Auth::check() and $user->isAdmin();
}
/**
@ -24,15 +25,14 @@ class AssignTicketToUserRequest extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'assign' => [
'nullable', 'exists:users,id',
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
function ($attribute, $value, $fail) {
/** @var \App\Models\User $user */
if ($user = User::whereId($value)->first() and !$user->isAdmin()) {
if ($user = User::query()->whereId($value)->first() and !$user->isAdmin()) {
$fail(trans('Cet utilisateur ne peut se voir assigner un ticket'));
}
}

View file

@ -12,7 +12,7 @@ class StoreBrandRequest extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
return Gate::allows('isAdmin');
}
@ -22,11 +22,11 @@ class StoreBrandRequest extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'name' => 'required|string|min:3|max:255',
'sites' => 'nullable|array',
'name' => 'required|string|min:3|max:255',
'sites' => 'nullable|array',
'sites.*.id' => 'required|exists:sites,id'
];
}
@ -39,8 +39,8 @@ class StoreBrandRequest extends FormRequest
public function attributes(): array
{
return [
'name' => trans('Nom de la marque'),
'sites' => trans('Sites associés à la marque'),
'name' => trans('Nom de la marque'),
'sites' => trans('Sites associés à la marque'),
'sites.*.id' => trans('Élément parmis les sites associés à la marque'),
];
}

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

@ -12,7 +12,7 @@ class ChangeTicketStatus extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
/** @var \App\Models\User */
$user = auth()->user();
@ -24,7 +24,7 @@ class ChangeTicketStatus extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'status' => [

View file

@ -12,7 +12,7 @@ class StoreCompanyRequest extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
return Gate::allows('isAdmin');
}
@ -22,7 +22,7 @@ class StoreCompanyRequest extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'name' => 'required|string|min:3|max:255',

View file

@ -4,6 +4,7 @@ namespace App\Http\Requests\Bo;
use App\Enums\MessageTypeEnum;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rules\Enum;
class CreateMessage extends FormRequest
@ -13,10 +14,11 @@ class CreateMessage extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
/** @var \App\Models\User|null */
if (!($user = auth()->user())) {
/** @var \App\Models\User|null $user */
$user = Auth::user();
if (!($user)) {
return false;
}
if (
@ -25,7 +27,7 @@ class CreateMessage extends FormRequest
) {
return false;
}
return auth()->check() and $user->sites and ($user->isAdmin() or $user->sites->contains(
return Auth::check() and $user->sites and ($user->isAdmin() or $user->sites->contains(
$this->route()->ticket->site_id
));
}
@ -35,10 +37,8 @@ class CreateMessage extends FormRequest
*
* @return void
*/
protected function prepareForValidation()
protected function prepareForValidation(): void
{
/** @var \App\Models\User */
$user = auth()->user();
$this->merge([
'subscribe' => $this->subscribe ? true : false,
'type' => $this->is_private ? MessageTypeEnum::private->value : MessageTypeEnum::public->value
@ -50,28 +50,15 @@ class CreateMessage extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
/** @var \App\Models\User */
$user = auth()->user();
/** @var \App\Models\User $user */
$user = Auth::user();
return [
'subscribe' => 'required|boolean',
'message' => [
'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"
);
}
}
},
],
'message' => 'required|min:' . ($user->isAdmin() ? '5' : '20'),
'file_messages' => 'sometimes|array',
'file_messages.*' => 'mimes:pptx,docx,xlsx,csv,txt,jpeg,jpg,png,pdf,svg|max:2048',
'file_messages.*' => 'mimes:pptx,docx,xlsx,csv,txt,jpeg,jpg,png,pdf|max:2048',
'type' => ['required', new Enum(MessageTypeEnum::class)]
];
}

View file

@ -11,7 +11,7 @@ class AnswerEstimateRequest extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
if (!auth()->check()) {
return false;
@ -26,7 +26,7 @@ class AnswerEstimateRequest extends FormRequest
*
* @return void
*/
protected function prepareForValidation()
protected function prepareForValidation(): void
{
$this->merge([
'answer' => $this->status === 'accept'
@ -38,7 +38,7 @@ class AnswerEstimateRequest extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'status' => 'required|in:accept,refuse',

View file

@ -12,7 +12,7 @@ class CreateEstimateRequest extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
if (!auth()->check()) {
return false;
@ -27,12 +27,12 @@ class CreateEstimateRequest extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
/** @var \App\Models\Ticket */
$ticket = $this->ticket;
return [
'amount' => [
'amount' => [
'required',
'numeric',
'gt:0',
@ -64,7 +64,7 @@ class CreateEstimateRequest extends FormRequest
public function attributes(): array
{
return [
'amount' => 'Montant',
'amount' => 'Montant',
'credit_account_id' => 'Compte à débiter'
];
}

View file

@ -14,7 +14,7 @@ class StoreCreditAccountRequest extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
return Gate::allows('isAdmin');
}
@ -24,11 +24,11 @@ class StoreCreditAccountRequest extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'label' => 'required|string|min:3|max:255',
'type' => [
'label' => 'required|string|min:3|max:255',
'type' => [
'required',
Rule::in(collect(AccountTypeEnum::cases())->pluck('value')->all()),
],
@ -44,8 +44,8 @@ class StoreCreditAccountRequest extends FormRequest
public function attributes(): array
{
return [
'label' => trans('Label du compte'),
'type' => trans('Type du compte'),
'label' => trans('Label du compte'),
'type' => trans('Type du compte'),
'balance' => trans('Solde du compte'),
];
}

View file

@ -12,7 +12,7 @@ class StoreCreditLogRequest extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
return Gate::allows('isAdmin');
}
@ -22,7 +22,7 @@ class StoreCreditLogRequest extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'amount' => [

View file

@ -14,7 +14,7 @@ class StoreInformationRequest extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
return Gate::allows('isAdmin');
}
@ -24,12 +24,12 @@ class StoreInformationRequest extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'title' => 'required|string|min:3|max:255',
'title' => 'required|string|min:3|max:255',
'message' => 'required|string|min:3|max:255',
'type' => [
'type' => [
'required',
Rule::in(collect(InfoColorsEnum::cases())->pluck('value')->all()),
],
@ -44,9 +44,9 @@ class StoreInformationRequest extends FormRequest
public function attributes(): array
{
return [
'title' => trans('Titre de l\'information'),
'title' => trans('Titre de l\'information'),
'message' => trans('Message de l\'information'),
'type' => trans('Type d\'information'),
'type' => trans('Type d\'information'),
];
}
}

View file

@ -15,7 +15,7 @@ class StoreInvoiceRequest extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
return Gate::allows('isAdmin');
}
@ -25,7 +25,7 @@ class StoreInvoiceRequest extends FormRequest
*
* @return void
*/
protected function prepareForValidation()
protected function prepareForValidation(): void
{
$this->merge([
'tasks' => request()->tasks ? \collect(json_decode(request()->tasks))
@ -41,7 +41,7 @@ class StoreInvoiceRequest extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'firstname' => 'required|string|min:3|max:255',
@ -73,25 +73,25 @@ class StoreInvoiceRequest extends FormRequest
public function attributes(): array
{
return [
'firstname' => trans('Prénom de la personne associée'),
'lastname' => trans('Nom de la personne associée'),
'object' => trans('Objet du devis'),
'tasks' => trans('Tâches du devis'),
'tasks.*' => trans('Tâches du devis'),
'tasks.*.days' => trans('Nombre de jours dédiés à la tâche'),
'tasks.*.amount' => trans('Montant dédié à la tâche'),
'tasks.*.desc' => trans('Description dédié à la tâche'),
'tasks.*.type' => trans('Type de tâche'),
'tasks.*.package' => trans('Forfait dédié à la tâche'),
'comment' => trans('Commentaire du devis'),
'show_days' => trans('Visibilité des jours'),
'site_id' => trans('Site associé'),
'company_id' => trans('Companie associé'),
'invoiced_to_id' => trans('Entité à facturer'),
'signed_date' => trans('Date de signature du Devis'),
'sent_client_date' => trans('Date d\'envoi du Devis au client'),
'firstname' => trans('Prénom de la personne associée'),
'lastname' => trans('Nom de la personne associée'),
'object' => trans('Objet du devis'),
'tasks' => trans('Tâches du devis'),
'tasks.*' => trans('Tâches du devis'),
'tasks.*.days' => trans('Nombre de jours dédiés à la tâche'),
'tasks.*.amount' => trans('Montant dédié à la tâche'),
'tasks.*.desc' => trans('Description dédié à la tâche'),
'tasks.*.type' => trans('Type de tâche'),
'tasks.*.package' => trans('Forfait dédié à la tâche'),
'comment' => trans('Commentaire du devis'),
'show_days' => trans('Visibilité des jours'),
'site_id' => trans('Site associé'),
'company_id' => trans('Companie associé'),
'invoiced_to_id' => trans('Entité à facturer'),
'signed_date' => trans('Date de signature du Devis'),
'sent_client_date' => trans('Date d\'envoi du Devis au client'),
'sent_accounting_date' => trans('Date d\'envoi du Devis à la comptabilité'),
'status' => trans('État du devis'),
'status' => trans('État du devis'),
];
}
}

View file

@ -12,7 +12,7 @@ class UpdateInvoiceRequest extends StoreInvoiceRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
$rules = [];
$model = $this->route('invoice');

View file

@ -13,7 +13,7 @@ class StoreLabelRequest extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
return Gate::allows('isAdmin');
}
@ -23,10 +23,10 @@ class StoreLabelRequest extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'name' => 'required|string|min:2|max:255|unique:labels,name',
'name' => 'required|string|min:2|max:255|unique:labels,name',
'color' => [
'required',
new Hex(),
@ -42,7 +42,7 @@ class StoreLabelRequest extends FormRequest
public function attributes(): array
{
return [
'name' => trans('validation.attributes.name'),
'name' => trans('validation.attributes.name'),
'color' => trans('validation.attributes.color'),
];
}

View file

@ -9,7 +9,7 @@ class UpdateLabelRequest extends StoreLabelRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
return array_merge(parent::rules(), [
'name' => 'required|string|min:2|max:255|unique:labels,name,' . $this->route('label')->id . ',id',

View file

@ -12,34 +12,21 @@ class StorePackageRequest extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
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.
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'name' => 'required|string|min:3|max:255',
'name' => 'required|string|min:3|max:255',
'amount' => 'required|numeric|min:0|regex:/^\d+(\.\d{1,2})?$/',
'published' => 'required|boolean',
];
}
@ -51,9 +38,8 @@ class StorePackageRequest extends FormRequest
public function attributes(): array
{
return [
'name' => trans('Nom'),
'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(): bool
{
return Gate::allows('isAdmin');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
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(): array
{
return array_merge(parent::rules(), [
'name' => 'required|string|min:3|max:255|unique:projects,name,' . $this->route('project')->id . ',id',
]);
}
}

View file

@ -12,7 +12,7 @@ class StoreSiteRequest extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
return Gate::allows('isAdmin');
}
@ -22,16 +22,16 @@ class StoreSiteRequest extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'name' => 'required|string|min:3|max:255',
'users' => 'nullable|array',
'users.*.id' => 'required|exists:users,id',
'gitlab_project_id' => 'nullable',
'name' => 'required|string|min:3|max:255',
'users' => 'nullable|array',
'users.*.id' => 'required|exists:users,id',
'gitlab_project_id' => 'nullable|unique:sites,gitlab_project_id',
'project_manager_id' => 'nullable|exists:users,id',
'url_re7' => 'nullable|url',
'url_prod' => 'nullable|url'
'url_re7' => 'nullable|url',
'url_prod' => 'nullable|url'
];
}
@ -43,13 +43,13 @@ class StoreSiteRequest extends FormRequest
public function attributes(): array
{
return [
'name' => trans('Nom du site'),
'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'),
'name' => trans('Nom du site'),
'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')
'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(): array
{
return array_merge(parent::rules(), [
'gitlab_project_id' => 'nullable|unique:sites,gitlab_project_id,' . $this->route('site')->id . ',id',
]);
}
}

View file

@ -12,7 +12,7 @@ class SortTickets extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
return auth()->check();
}
@ -22,12 +22,12 @@ class SortTickets extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'search' => 'nullable|string',
'search' => 'nullable|string',
'sort_way' => 'sometimes|in:asc,desc',
'status' => [
'status' => [
'sometimes',
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
function ($attribute, $value, $fail) {
@ -38,7 +38,7 @@ class SortTickets extends FormRequest
}
}
],
'pr' => 'sometimes|int|exists:sites,id'
'pr' => 'sometimes|int|exists:sites,id'
];
}
}

View file

@ -14,7 +14,7 @@ class StoreTicket extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
return auth()->check();
}
@ -24,7 +24,7 @@ class StoreTicket extends FormRequest
*
* @return void
*/
protected function prepareForValidation()
protected function prepareForValidation(): void
{
/** @var \App\Models\User|null */
$user = auth()->user();
@ -97,24 +97,10 @@ 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"
);
}
}
},
],
'text' => 'required|string|min:40',
'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',
'file_tickets.*' => 'mimes:pptx,docx,xlsx,csv,txt,jpeg,jpg,png,pdf|max:2048',
'is_internal' => 'required|boolean',
'labels' => 'sometimes|array',
'labels.*' => [

View file

@ -37,7 +37,7 @@ class UpdateTicketTypeRequest extends FormRequest
public function attributes(): array
{
return [
'type' => trans('Type'),
'type' => trans('Type'),
'template' => trans('Template'),
];
}

View file

@ -2,6 +2,8 @@
namespace App\Http\Requests\Bo;
use Illuminate\Support\Facades\Auth;
class UpdateTicket extends StoreTicket
{
/**
@ -11,11 +13,12 @@ class UpdateTicket extends StoreTicket
*/
public function authorize(): bool
{
/** @var \App\Models\User|null */
if (!($user = auth()->user())) {
/** @var \App\Models\User|null $user */
$user = Auth::user();
if (!($user)) {
return false;
}
return auth()->check() and ($user->isAdmin() or
return Auth::check() and ($user->isAdmin() or
optional(request()->route()->ticket)->created_by === $user->id
);
}

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

@ -14,44 +14,28 @@ class StoreUserRequest extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
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.
*
* @return array
*/
public function rules()
public function rules(): array
{
return [
'name' => 'required|string|min:3|max:255',
'email' => [
'email',
'required',
Rule::unique('users', 'email')->whereNull('deleted_at')
],
'role' => [
'name' => 'required|string|min:3|max:255',
'email' => 'email|required|unique:users,email',
'role' => [
'required',
Rule::in(collect(RoleEnum::cases())->pluck('value')->all()),
],
'sites' => 'nullable|array',
'sites' => 'nullable|array',
'sites.*.id' => 'required|exists:sites,id',
'gitlab_id' => 'nullable|integer|unique:users,gitlab_id'
'gitlab_id' => 'nullable|integer|unique:users,gitlab_id'
];
}
@ -63,12 +47,12 @@ class StoreUserRequest extends FormRequest
public function attributes(): array
{
return [
'name' => trans('Nom de l\'utilisateur'),
'email' => trans('Email de l\'utilisateur'),
'role' => trans('Rôle de l\'utilisateur'),
'sites' => trans('Sites associés à l\'utilisateur'),
'name' => trans('Nom de l\'utilisateur'),
'email' => trans('Email de l\'utilisateur'),
'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")
'gitlab_id' => trans("ID de l'utilisateur Gitlab")
];
}
}

View file

@ -9,11 +9,10 @@ class UpdateUserRequest extends StoreUserRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
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

@ -12,17 +12,17 @@ class UserLabeledResource extends ResourceCollection
* Transform the resource collection into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
* @return \Illuminate\Support\Collection
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
*/
public function toArray(Request $request)
public function toArray(Request $request): \Illuminate\Support\Collection
{
// phpcs:enable
return $this->collection->mapWithKeys(function (User $user, $key) {
return [
$key => [
'label' => sprintf('%d %s', $user->id, $user->name),
'key' => $user->id
'key' => $user->id
]
];
});

View file

@ -6,11 +6,12 @@ interface SpsConfigInterface
{
/**
* URL method
* Website (SP) url ex: https://le-gaulois-pro.s-web.test
* Website (SP) url ex: https://le-gaulois-pro.s-web.test
*
* @return string
*/
public function url(): string;
/**
* acsBase64 method
* Return destination url base64 encoded
@ -18,6 +19,7 @@ interface SpsConfigInterface
* @return string
*/
public function acsBase64(): string;
/**
* destination method
* Return acs url concatenated with url
@ -25,6 +27,7 @@ interface SpsConfigInterface
* @return string
*/
public function destination(): string;
/**
* logout method
* Return sls url concatenated with url
@ -32,6 +35,7 @@ interface SpsConfigInterface
* @return string
*/
public function logout(): string;
/**
* certificate method
* Return certificate (.pem) file from kebab case of Enum name

View file

@ -15,7 +15,7 @@ class AutoCloseOldTickets
*
* @return void
*/
public function handle()
public function handle(): void
{
$limitDays = 15;
$message = 'En l\'absence d\'activitée depuis 15 jours ce ticket est automatiquement clos et archivé.';

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

@ -39,7 +39,7 @@ class SendTicketMentionMail implements ShouldQueue
*
* @return void
*/
public function handle()
public function handle(): void
{
Mail::to($this->user->email)->send($this->mail);
Log::debug(sprintf('Mention mail sent to %s', $this->user->email));

View file

@ -36,7 +36,7 @@ class SendUserCreatedEmail implements ShouldQueue
*
* @return void
*/
public function handle()
public function handle(): void
{
Mail::to(['email' => $this->user->email])
->send(new UserCreatedMail($this->user));

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

@ -15,7 +15,7 @@ class WarnBeforeAutoCloseTicket
*
* @return void
*/
public function handle()
public function handle(): void
{
$warningDays = 7;
// phpcs:ignore Generic.Files.LineLength.TooLong

View file

@ -16,7 +16,7 @@ class CollectionHelper
* @param integer $pageSize
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
public static function paginate(Collection $results, int $pageSize): LengthAwarePaginator
public static function paginate(Collection $results, int $pageSize): \Illuminate\Pagination\LengthAwarePaginator
{
$page = Paginator::resolveCurrentPage('page');
@ -31,8 +31,11 @@ class CollectionHelper
* @param integer $page
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
public static function paginateManually(Collection $results, int $pageSize, int $page = 1): LengthAwarePaginator
{
public static function paginateManually(
Collection $results,
int $pageSize,
int $page = 1
): \Illuminate\Pagination\LengthAwarePaginator {
$total = $results->count();
return self::paginator($results->forPage($page, $pageSize), $total, $pageSize, $page, [
@ -51,14 +54,19 @@ class CollectionHelper
* @param array $options
* @return \Illuminate\Pagination\LengthAwarePaginator
*/
protected static function paginator(Collection $items, int $total, int $perPage, int $currentPage, array $options)
{
protected static function paginator(
Collection $items,
int $total,
int $perPage,
int $currentPage,
array $options
): \Illuminate\Pagination\LengthAwarePaginator {
return Container::getInstance()->makeWith(LengthAwarePaginator::class, [
'items' => $items,
'total' => $total,
'perPage' => $perPage,
'items' => $items,
'total' => $total,
'perPage' => $perPage,
'currentPage' => $currentPage,
'options' => $options
'options' => $options
]);
}
}

View file

@ -5,7 +5,7 @@ namespace App\Lib\Helpers;
class ExceptionsHelper
{
/**
* Handle sentry event
* Handle sentry event.
*
* @param \Sentry\Event $event
* @return null|\Sentry\Event
@ -26,7 +26,7 @@ class ExceptionsHelper
}
/**
* Anonimize Sentry Exception bag
* Anonimize Sentry Exception bag.
*
* @param \Sentry\ExceptionDataBag $exceptionDataBag
* @return \Sentry\ExceptionDataBag
@ -42,7 +42,7 @@ class ExceptionsHelper
}
/**
* Anonymise sentry stacktrace
* Anonymise sentry stacktrace.
*
* @param \Sentry\Stacktrace $stackTrace
* @return \Sentry\Stacktrace
@ -70,7 +70,7 @@ class ExceptionsHelper
}
/**
* Get anonymized sentry params
* Get anonymized sentry params.
*
* @param mixed[] $params
* @return string[]
@ -105,7 +105,7 @@ class ExceptionsHelper
/**
* Anonymize at least Sql queries
* Anonymize at least Sql queries.
*
* @param string $message
* @return string
@ -120,7 +120,7 @@ class ExceptionsHelper
}
/**
* Anonymize stack trace
* Anonymize stack trace.
*
* @param \Throwable $e
* @return string
@ -159,7 +159,7 @@ class ExceptionsHelper
}
/**
* Handle Args
* Handle Args.
*
* @param array $stack
* @return string

View file

@ -175,7 +175,7 @@ class FileStorageHelper
/**
* Remove all old files from string attribute html that are stored in public that
* are nove present anymore on new html string
* are nove present anymore on new html string.
*
* @param string $newHtmlString The html string with alreadyParsed files using storeModelFilesFromHtml.
* @param string $oldHtmlString The old html string with alreadyParsed files using storeModelFilesFromHtml.
@ -188,7 +188,7 @@ class FileStorageHelper
/**
* Remove all old files that are stored in public that
* are not present anymore on new array files
* are not present anymore on new array files.
*
* @param string[] $oldFiles The old array files.
* @param string[] $newFiles The new array files.
@ -208,7 +208,7 @@ class FileStorageHelper
}
/**
* Remove all files from string attribute html that are stored in public
* Remove all files from string attribute html that are stored in public.
*
* @param string $htmlString The html string with alreadyParsed files using storeModelFilesFromHtml.
* @return void
@ -225,7 +225,7 @@ class FileStorageHelper
}
/**
* Find all occurence of files in html
* Find all occurence of files in html.
*
* @param string $html
* @return array
@ -241,7 +241,7 @@ class FileStorageHelper
/**
* Filter array to get only uploaded files that exists
* on disk 'public' (using helper storage strategy)
* on disk 'public' (using helper storage strategy).
*
* @param array $urls
* @return array
@ -261,8 +261,7 @@ class FileStorageHelper
}
/**
* Filter array to get only uploaded files that exists
* on disk 'uploads'
* Filter array to get only uploaded files that exists on disk 'uploads'.
*
* @param array $urls
* @return array
@ -284,8 +283,7 @@ class FileStorageHelper
}
/**
* Prepare storage path and return
* the file full path
* Prepare storage path and return the file full path.
*
* @param string $tableName Model table name.
* @param string $filename The basename for the file.
@ -316,7 +314,7 @@ class FileStorageHelper
/**
* Create sub folders to store files using pattern
* storage/modelfiles/posts/10/04/19/5f
* year/month/day/md5 first 2 letters
* year/month/day/md5 first 2 letters.
*
* @param string $storage_dir
* @param string $table
@ -342,7 +340,7 @@ class FileStorageHelper
}
/**
* Get unique filename if the path is already occupied
* Get unique filename if the path is already occupied.
*
* @param string $folderPath
* @param string $filename
@ -365,7 +363,7 @@ class FileStorageHelper
}
/**
* Slugify the filename
* Slugify the filename.
*
* @param string $filename
* @return string
@ -376,7 +374,7 @@ class FileStorageHelper
}
/**
* Sanitize File name for uploaded files
* Sanitize File name for uploaded files.
*
* @param string $filename The original uploaded client file name.
* @param boolean $beautify Beautify the filename.
@ -402,7 +400,7 @@ class FileStorageHelper
$filename = Str::of($filename)->ltrim('.-');
// * Optional beautification
if ($beautify) {
$filename = static::beautifyFilename($filename);
$filename = self::beautifyFilename($filename);
}
// * Maximize filename length to 255 bytes http://serverfault.com/a/9548/44086
/** @var string */
@ -421,7 +419,7 @@ class FileStorageHelper
}
/**
* Beautify a filename
* Beautify a filename.
*
* @param string $filename
* @return string

View file

@ -2,8 +2,8 @@
namespace App\Lib\Helpers;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
@ -57,7 +57,7 @@ class ToolboxHelper
}
/**
* Check uri points to image file
* Check uri points to image file.
*
* @param string $url
* @return boolean
@ -69,7 +69,7 @@ class ToolboxHelper
}
/**
* Check uri points to pdf file
* Check uri points to pdf file.
*
* @param string $url
* @return boolean
@ -85,7 +85,7 @@ class ToolboxHelper
* Only embed allowed are for MP4 self hosted videos right now
*
* https is force by ckeditor MediaElements which natively supports
* only cloud hosted medias (youtube, spotify etc..)
* only cloud hosted medias (youtube, spotify etc..).
*
* @param string $input
* @return string
@ -97,7 +97,7 @@ class ToolboxHelper
}
/**
* Assert That no Back Office url is present on txt
* Assert That no Back Office url is present on txt.
*
* @param string $field
* @param string $text
@ -140,7 +140,7 @@ class ToolboxHelper
}
/**
* Assert That only external url is present on txt
* Assert That only external url is present on txt.
*
* @param string $field
* @param string $text
@ -184,7 +184,7 @@ class ToolboxHelper
}
/**
* Assert Is Unique
* Assert Is Unique.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param array<string> $columns
@ -235,7 +235,7 @@ class ToolboxHelper
}
/**
* Mutltibyte string replace
* Mutltibyte string replace.
*
* @param array|string $search
* @param array|string $replace
@ -279,7 +279,7 @@ class ToolboxHelper
}
/**
* Asserts that the field is unique
* Asserts that the field is unique.
*
* @param string $table
* @param string $field
@ -295,7 +295,7 @@ class ToolboxHelper
string|int|float|bool|null|array $value,
?int $id = null,
?callable $query = null
) {
): void {
if (!$query) {
Validator::make([$field => $value], [
self::mbReplace('.', '\.', $field) => Rule::unique($table, $field)->ignore($id),
@ -316,7 +316,7 @@ class ToolboxHelper
}
/**
* Get original polyfill with autotranslate behavior
* Get original polyfill with autotranslate behavior.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $attribute
@ -335,20 +335,20 @@ class ToolboxHelper
}
/**
* Dynamic model query
* Dynamic model query.
*
* @param string $defaultLocale
* @param \Illuminate\Contracts\Database\Eloquent\Builder $q
* @param string $column
* @param string $value
* @return \Illuminate\Contracts\Database\Eloquent\Builder
* @param string $defaultLocale
* @param \Illuminate\Database\Eloquent\Builder $q
* @param string $column
* @param string $value
* @return \Illuminate\Database\Eloquent\Builder
*/
public static function queryColumnWithLocales(
string $defaultLocale,
Builder $q,
string $column,
string $value
): Builder {
): \Illuminate\Contracts\Database\Eloquent\Builder {
$currentLocale = app()->currentLocale();
$locales = collect(config('app.locales'))
->mapWithKeys(fn(string $locale) => [$locale => $locale]);
@ -376,7 +376,7 @@ class ToolboxHelper
}
/**
* Generates an array from a dot separated string
* Generates an array from a dot separated string.
*
* @param string $path
* @param mixed $value
@ -395,7 +395,7 @@ class ToolboxHelper
}
/**
* Convert deeply an object to array
* Convert deeply an object to array.
*
* @param array|object $input
* @return array
@ -418,7 +418,7 @@ class ToolboxHelper
}
/**
* Convert deeply an array to object
* Convert deeply an array to object.
*
* @param array|object $input
* @return object
@ -441,15 +441,15 @@ class ToolboxHelper
}
/**
* Validate picture
* Validate picture.
*
* @param string $attribute
* @param string|null $value
* @param callback $fail
* @param callable $fail
* @param object $sizes
* @return void
*/
public static function validatePicture(string $attribute, string|null $value, callable $fail, object $sizes)
public static function validatePicture(string $attribute, string|null $value, callable $fail, object $sizes): void
{
if (!($useStorage = Storage::exists($value)) and !File::exists($value)) {
$fail(trans(':attribute n\'existe pas dans le système de fichier', [
@ -502,7 +502,7 @@ class ToolboxHelper
}
/**
* Get the file full path
* Get the file full path.
*
* @param string $value
* @param boolean $useStorage
@ -516,13 +516,13 @@ class ToolboxHelper
}
/**
* Get the file mimetype
* Get the file mimetype.
*
* @param string $value
* @param boolean $useStorage
* @return string|false
*/
private static function getUploadedFileMimeType(string $value, bool $useStorage)
private static function getUploadedFileMimeType(string $value, bool $useStorage): string|false
{
return !$useStorage ? File::mimeType($value) : Storage::mimeType($value);
}
@ -559,6 +559,6 @@ class ToolboxHelper
]
);
return \response()->json($models ?? 500);
return \response()->json($models);
}
}

View file

@ -16,7 +16,7 @@ class AddStatusMessages
* @return void
* @phpcs:disable Generic.Metrics.CyclomaticComplexity
*/
public function handle(TicketStatusChanged $event)
public function handle(TicketStatusChanged $event): void
{
// phpcs:enable
$message = new Message();

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): void
{
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

@ -14,10 +14,12 @@ class SendNotificationMailToTicketCreator
* @param \App\Events\NotifyTicketCreator $event
* @return void
*/
public function handle(NotifyTicketCreator $event)
public function handle(NotifyTicketCreator $event): void
{
$ticket = $event->ticket;
Mail::to($ticket->user()->withTrashed()->first()->email)
/** @var \App\Models\User */
$user = $ticket->user();
Mail::to($user->withTrashed()->first()->email)
->send(new TicketUpdate($ticket, $event->corpsMaj));
}
}

View file

@ -16,7 +16,7 @@ class SendSubscriptionsNotificationsMail
* @param \App\Events\NotifySubscriptions $event
* @return void
*/
public function handle(NotifySubscriptions $event)
public function handle(NotifySubscriptions $event): void
{
$ignoreConnectedUserEmail = Auth::user()->email ?? '';
$ignoreUsers = $event->ignoreUsers ?? collect();

View file

@ -2,39 +2,39 @@
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.
*
* @return $this
*/
public function build()
public function build(): self
{
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

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

View file

@ -37,11 +37,13 @@ class TicketUpdate extends Mailable
*
* @return $this
*/
public function build()
public function build(): self
{
$ticket = $this->ticket;
$ticket = $this->ticket;
/** @var \App\Models\Site */
$site = $ticket->site;
$corpsMaj = \html_entity_decode(ToolboxHelper::plainToHtml($this->corpsMaj));
return $this->subject("[Support][{$ticket->site->name}] Ticket n°$ticket->id - $ticket->title")
return $this->subject("[Support][{$site->name}] Ticket n°$ticket->id")
->view('emails.ticket-update', compact('ticket', 'corpsMaj'));
}
}

View file

@ -26,7 +26,7 @@ class UserCreatedMail extends Mailable
*
* @return $this
*/
public function build()
public function build(): self
{
return $this
->subject(sprintf('%s : Votre compte a été crée', config('app.name')))

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
{
@ -22,7 +16,7 @@ class Brand extends Model
/**
* The attributes that are fillable.
*
* @var array
* @var list<string>
*/
protected $fillable = [
'name'
@ -56,6 +50,7 @@ class Brand extends Model
*/
public static function deleteSites(Brand $brand): void
{
/** @var \App\Models\Site $site */
foreach ($brand->sites as $site) {
$site->brand_id = null;
$site->saveOrFail();
@ -69,7 +64,7 @@ class Brand extends Model
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function sites()
public function sites(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Site::class);
}
@ -79,7 +74,7 @@ class Brand extends Model
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function accounts()
public function accounts(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Account::class);
}

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
{
@ -23,7 +16,7 @@ class Company extends Model
/**
* The attributes that are fillable.
*
* @var array
* @var list<string>
*/
protected $fillable = [
'name'
@ -32,28 +25,28 @@ class Company extends Model
/**
* The accessors to append to the model's array form.
*
* @var array
* @var list<string>
*/
protected $appends = [
'value'
];
/**
* HasMany Invoices
* HasMany Invoices.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function invoices()
public function invoices(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Invoice::class);
}
/**
* Return value attribute for belongsto search component
* Return value attribute for belongsto search component.
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
protected function value(): Attribute
protected function value(): \Illuminate\Database\Eloquent\Casts\Attribute
{
return new Attribute(
get: fn () => $this->id,

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