feat(absences) : outils MCP CRUD pour les absences

Expose le module Absences via le serveur MCP et comble les trous CRUD
existants (projets, groupes, métadonnées de tâches, clients, users RH).

Absences (réutilise AbsenceDayCalculator + AbsenceBalanceService pour ne
pas contourner la logique de soldes) :
- list/get/create/review/cancel/delete-absence-request
- list/update-absence-policy, list/update-absence-balance
- create-absence-request prend un userId explicite (agir au nom d'un employé) ;
  review/cancel maintiennent les soldes (pending/taken) cohérents
- AbsenceRequestRepository::findFiltered pour les filtres de liste

Trous CRUD comblés :
- delete-project, delete-group
- CRUD tag, effort, priority
- CRUD status (couplé au workflow, avec category)
- CRUD client, get/update-user (champs RH, sans password ni roles)

Sérialisation centralisée (Serializer::absenceRequest/Policy/Balance/client/userFull).
Instructions MCP (mcp.yaml) mises à jour : statuts par workflow + domaine absences.

Tests : tests/Functional/Mcp/AbsenceRequestLifecycleTest (création / approbation /
annulation admin) vérifient le cycle complet et la cohérence des soldes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-05-22 14:10:56 +02:00
parent 2a0b202d32
commit 2b148fa65a
36 changed files with 4536 additions and 1 deletions
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Mcp\Tool\TaskMeta;
use App\Enum\StatusCategory;
use App\Repository\TaskStatusRepository;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use Mcp\Capability\Attribute\McpTool;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use function sprintf;
#[McpTool(name: 'update-status', description: 'Update a task status (admin). Only provided fields change. category = todo|in_progress|blocked|review|done.')]
class UpdateStatusTool
{
public function __construct(
private readonly TaskStatusRepository $statusRepository,
private readonly EntityManagerInterface $entityManager,
private readonly Security $security,
) {}
public function __invoke(
int $id,
?string $label = null,
?string $category = null,
?string $color = null,
?int $position = null,
?bool $isFinal = null,
): string {
if (!$this->security->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException('Access denied: ROLE_ADMIN required.');
}
$status = $this->statusRepository->find($id);
if (null === $status) {
throw new InvalidArgumentException(sprintf('TaskStatus with ID %d not found.', $id));
}
if (null !== $label) {
$status->setLabel($label);
}
if (null !== $category) {
$status->setCategory(
StatusCategory::tryFrom($category)
?? throw new InvalidArgumentException(sprintf('Unknown status category "%s".', $category)),
);
}
if (null !== $color) {
$status->setColor($color);
}
if (null !== $position) {
$status->setPosition($position);
}
if (null !== $isFinal) {
$status->setIsFinal($isFinal);
}
$this->entityManager->flush();
return json_encode([
'id' => $status->getId(),
'label' => $status->getLabel(),
'color' => $status->getColor(),
'position' => $status->getPosition(),
'isFinal' => $status->getIsFinal(),
'category' => $status->getCategory()->value,
'workflowId' => $status->getWorkflow()?->getId(),
]);
}
}