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>
105 lines
4.2 KiB
PHP
105 lines
4.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Mcp\Tool\Absence;
|
|
|
|
use App\Entity\AbsenceRequest;
|
|
use App\Enum\AbsenceStatus;
|
|
use App\Enum\AbsenceType;
|
|
use App\Enum\HalfDay;
|
|
use App\Mcp\Tool\Serializer;
|
|
use App\Repository\AbsencePolicyRepository;
|
|
use App\Repository\AbsenceRequestRepository;
|
|
use App\Repository\UserRepository;
|
|
use App\Service\AbsenceBalanceService;
|
|
use App\Service\AbsenceDayCalculator;
|
|
use DateTimeImmutable;
|
|
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: 'create-absence-request', description: 'Create an absence request on behalf of an employee (userId). Validates active policy + no overlap, computes deducted working days, sets status=pending and reserves the days in the pending balance. type: cp|mariage_pacs|conge_parental|deces|maladie. Dates YYYY-MM-DD. halfDay (matin|apres_midi) on a boundary subtracts 0.5.')]
|
|
class CreateAbsenceRequestTool
|
|
{
|
|
public function __construct(
|
|
private readonly EntityManagerInterface $entityManager,
|
|
private readonly UserRepository $userRepository,
|
|
private readonly AbsencePolicyRepository $policyRepository,
|
|
private readonly AbsenceRequestRepository $requestRepository,
|
|
private readonly AbsenceDayCalculator $calculator,
|
|
private readonly AbsenceBalanceService $balanceService,
|
|
private readonly Security $security,
|
|
) {}
|
|
|
|
public function __invoke(
|
|
int $userId,
|
|
string $type,
|
|
string $startDate,
|
|
string $endDate,
|
|
?string $startHalfDay = null,
|
|
?string $endHalfDay = null,
|
|
?string $reason = null,
|
|
): string {
|
|
if (!$this->security->isGranted('ROLE_USER')) {
|
|
throw new AccessDeniedException('Access denied: ROLE_USER required.');
|
|
}
|
|
|
|
$user = $this->userRepository->find($userId)
|
|
?? throw new InvalidArgumentException(sprintf('User with ID %d not found.', $userId));
|
|
|
|
$typeEnum = AbsenceType::tryFrom($type)
|
|
?? throw new InvalidArgumentException(sprintf('Unknown absence type "%s".', $type));
|
|
|
|
$start = new DateTimeImmutable($startDate);
|
|
$end = new DateTimeImmutable($endDate);
|
|
if ($end < $start) {
|
|
throw new InvalidArgumentException('End date must be on or after start date.');
|
|
}
|
|
|
|
$startHd = null !== $startHalfDay
|
|
? (HalfDay::tryFrom($startHalfDay) ?? throw new InvalidArgumentException(sprintf('Unknown half day "%s".', $startHalfDay)))
|
|
: null;
|
|
$endHd = null !== $endHalfDay
|
|
? (HalfDay::tryFrom($endHalfDay) ?? throw new InvalidArgumentException(sprintf('Unknown half day "%s".', $endHalfDay)))
|
|
: null;
|
|
|
|
$policy = $this->policyRepository->findOneByType($typeEnum);
|
|
if (null === $policy || !$policy->isActive()) {
|
|
throw new InvalidArgumentException('This absence type is not available.');
|
|
}
|
|
|
|
if ($this->requestRepository->hasOverlap($user, $start, $end)) {
|
|
throw new InvalidArgumentException('This request overlaps an existing absence.');
|
|
}
|
|
|
|
$countedDays = $this->calculator->countWorkingDays($start, $end, $startHd, $endHd, $policy->isCountWorkingDaysOnly());
|
|
if ($countedDays <= 0.0) {
|
|
throw new InvalidArgumentException('The selected range contains no working day.');
|
|
}
|
|
|
|
$request = new AbsenceRequest();
|
|
$request->setUser($user);
|
|
$request->setType($typeEnum);
|
|
$request->setStartDate($start);
|
|
$request->setEndDate($end);
|
|
$request->setStartHalfDay($startHd);
|
|
$request->setEndHalfDay($endHd);
|
|
$request->setReason($reason);
|
|
$request->setCountedDays($countedDays);
|
|
$request->setStatus(AbsenceStatus::Pending);
|
|
$request->setRejectionReason(null);
|
|
$request->setCreatedAt(new DateTimeImmutable());
|
|
|
|
$this->entityManager->persist($request);
|
|
$this->balanceService->reservePending($request);
|
|
$this->entityManager->flush();
|
|
|
|
return json_encode(Serializer::absenceRequest($request));
|
|
}
|
|
}
|