feat(absences) : avancement module absences + suppression du portail client

Deux lots regroupés sur la branche feat/absence-management.

Suppression complète du portail client :
- retire ROLE_CLIENT (security.yaml) ; User::getRoles() ajoute toujours ROLE_USER
- supprime l'entité ClientTicket (+ repo, states, relations), User.client et
  User.allowedProjects, NotificationService, ProjectAllowedExtension, le bloc
  ROLE_CLIENT de MailAccessChecker
- front : pages /portal, layout portal, composants client-ticket/,
  AdminClientTicketTab, services/dto/i18n/docs associés
- fixtures : retire les users client-liot / client-acme
- migration Version20260522110000 (drop client_ticket, user_allowed_projects,
  colonnes liées ; task_document.task_id -> NOT NULL)
- tests : retire les cas obsolètes testant le blocage des clients sur le mail

Module gestion des absences (WIP) :
- entités / migrations (Version20260521160000, Version20260522090000)
- pages absences.vue / team-absences.vue, composants frontend/components/absence/
- services front, AccrueLeaveCommand, PublicHolidayController

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-05-22 11:31:31 +02:00
parent de98924fd3
commit 2a0b202d32
109 changed files with 3918 additions and 3656 deletions
+6 -32
View File
@@ -6,14 +6,12 @@ namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\ClientTicket;
use App\Entity\Task;
use App\Entity\TaskDocument;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Uid\Uuid;
@@ -81,39 +79,16 @@ final readonly class TaskDocumentProcessor implements ProcessorInterface
throw new BadRequestHttpException('File size exceeds 50 MB limit.');
}
$taskIri = $request->request->get('task', '');
$clientTicketIri = $request->request->get('clientTicket', '');
$taskIri = $request->request->get('task', '');
if ('' === $taskIri && '' === $clientTicketIri) {
throw new BadRequestHttpException('Either task or clientTicket IRI is required.');
if ('' === $taskIri) {
throw new BadRequestHttpException('A task IRI is required.');
}
$task = null;
$clientTicket = null;
$task = $this->entityManager->getRepository(Task::class)->find((int) basename($taskIri));
if ('' !== $taskIri) {
// ROLE_CLIENT (without ROLE_ADMIN) cannot upload documents directly to tasks
if ($this->security->isGranted('ROLE_CLIENT') && !$this->security->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedHttpException('Clients can only upload documents to client tickets.');
}
$task = $this->entityManager->getRepository(Task::class)->find((int) basename($taskIri));
if (null === $task) {
throw new BadRequestHttpException('Task not found.');
}
}
if ('' !== $clientTicketIri) {
$clientTicket = $this->entityManager->getRepository(ClientTicket::class)->find((int) basename($clientTicketIri));
if (null === $clientTicket) {
throw new BadRequestHttpException('Client ticket not found.');
}
if (!$this->security->isGranted('ROLE_ADMIN') && $clientTicket->getSubmittedBy() !== $this->security->getUser()) {
throw new AccessDeniedHttpException('You can only upload documents to your own tickets.');
}
if (null === $task) {
throw new BadRequestHttpException('Task not found.');
}
// Use server-detected MIME type (finfo), not the client-supplied one
@@ -137,7 +112,6 @@ final readonly class TaskDocumentProcessor implements ProcessorInterface
$document = new TaskDocument();
$document->setTask($task);
$document->setClientTicket($clientTicket);
$document->setOriginalName($originalName);
$document->setFileName($fileName);
$document->setMimeType($mimeType);