fix : correction des Heures et ajout d'une validation pour les chefs de site
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
This commit is contained in:
@@ -9,6 +9,7 @@ use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Entity\Absence;
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\User;
|
||||
use App\Enum\HalfDay;
|
||||
use App\Repository\Contract\AbsenceReadRepositoryInterface;
|
||||
use App\Repository\Contract\WorkHourReadRepositoryInterface;
|
||||
@@ -16,7 +17,9 @@ use DateInterval;
|
||||
use DatePeriod;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
|
||||
@@ -26,6 +29,7 @@ final readonly class AbsenceWriteProcessor implements ProcessorInterface
|
||||
private EntityManagerInterface $entityManager,
|
||||
private AbsenceReadRepositoryInterface $absenceRepository,
|
||||
private WorkHourReadRepositoryInterface $workHourRepository,
|
||||
private Security $security,
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
||||
@@ -39,8 +43,11 @@ final readonly class AbsenceWriteProcessor implements ProcessorInterface
|
||||
return $data;
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
$isAdmin = $user instanceof User && in_array('ROLE_ADMIN', $user->getRoles(), true);
|
||||
|
||||
if ($operation instanceof DeleteOperationInterface) {
|
||||
if ($this->workHourRepository->hasValidatedInRange($employee, $data->getStartDate(), $data->getEndDate())) {
|
||||
if ($this->isLockedByValidation($employee, $data->getStartDate(), $data->getEndDate(), $isAdmin)) {
|
||||
throw new ConflictHttpException('Impossible de modifier une absence sur une période validée.');
|
||||
}
|
||||
|
||||
@@ -58,7 +65,7 @@ final readonly class AbsenceWriteProcessor implements ProcessorInterface
|
||||
$from = DateTimeImmutable::createFromInterface($segments[0]['date']);
|
||||
$to = DateTimeImmutable::createFromInterface($segments[count($segments) - 1]['date']);
|
||||
|
||||
if ($this->workHourRepository->hasValidatedInRange($employee, $from, $to)) {
|
||||
if ($this->isLockedByValidation($employee, $from, $to, $isAdmin)) {
|
||||
throw new ConflictHttpException('Impossible de modifier une absence sur une période validée.');
|
||||
}
|
||||
|
||||
@@ -178,6 +185,19 @@ final readonly class AbsenceWriteProcessor implements ProcessorInterface
|
||||
return DateTime::createFromImmutable($date);
|
||||
}
|
||||
|
||||
private function isLockedByValidation(Employee $employee, DateTimeInterface $from, DateTimeInterface $to, bool $isAdmin): bool
|
||||
{
|
||||
if ($this->workHourRepository->hasValidatedInRange($employee, $from, $to)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($isAdmin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->workHourRepository->hasSiteValidatedInRange($employee, $from, $to);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{date: DateTimeImmutable, startHalf: HalfDay, endHalf: HalfDay} $segment
|
||||
*/
|
||||
@@ -193,6 +213,8 @@ final readonly class AbsenceWriteProcessor implements ProcessorInterface
|
||||
$workHour
|
||||
->setMorningFrom(null)
|
||||
->setMorningTo(null)
|
||||
->setIsSiteValid(false)
|
||||
->setIsValid(false)
|
||||
;
|
||||
|
||||
return;
|
||||
@@ -205,6 +227,8 @@ final readonly class AbsenceWriteProcessor implements ProcessorInterface
|
||||
->setAfternoonTo(null)
|
||||
->setEveningFrom(null)
|
||||
->setEveningTo(null)
|
||||
->setIsSiteValid(false)
|
||||
->setIsValid(false)
|
||||
;
|
||||
|
||||
return;
|
||||
@@ -218,6 +242,8 @@ final readonly class AbsenceWriteProcessor implements ProcessorInterface
|
||||
->setAfternoonTo(null)
|
||||
->setEveningFrom(null)
|
||||
->setEveningTo(null)
|
||||
->setIsSiteValid(false)
|
||||
->setIsValid(false)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ final readonly class EmployeeWriteProcessor implements ProcessorInterface
|
||||
|
||||
$today = new DateTimeImmutable('today');
|
||||
if ($isNew) {
|
||||
$this->ensureContractPeriodExists($data, $currentContract, $today);
|
||||
$this->ensureContractPeriodExists($data, $currentContract, new DateTimeImmutable('1970-01-01'));
|
||||
|
||||
return $result;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ final readonly class EmployeeWriteProcessor implements ProcessorInterface
|
||||
}
|
||||
|
||||
$todayPeriod = $this->periodRepository->findOneCoveringDate($data, $today);
|
||||
if (null !== $todayPeriod && null === $todayPeriod->getEndDate() && $todayPeriod->getStartDate() === $today) {
|
||||
if (null !== $todayPeriod && null === $todayPeriod->getEndDate() && $todayPeriod->getStartDate()->format('Y-m-d') === $today->format('Y-m-d')) {
|
||||
$todayPeriod->setContract($currentContract);
|
||||
$this->entityManager->flush();
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\ApiResource\WorkHourBulkUpsertResult;
|
||||
use App\Entity\User;
|
||||
use App\Entity\WorkHour;
|
||||
use App\Enum\TrackingMode;
|
||||
use App\Repository\Contract\AbsenceReadRepositoryInterface;
|
||||
use App\Repository\EmployeeRepository;
|
||||
use App\Repository\WorkHourRepository;
|
||||
use App\Service\Contracts\EmployeeContractResolver;
|
||||
@@ -28,6 +29,7 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
|
||||
private Security $security,
|
||||
private EmployeeRepository $employeeRepository,
|
||||
private WorkHourRepository $workHourRepository,
|
||||
private AbsenceReadRepositoryInterface $absenceRepository,
|
||||
private EmployeeContractResolver $contractResolver,
|
||||
) {}
|
||||
|
||||
@@ -67,6 +69,13 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
|
||||
$existingByEmployeeId = $this->workHourRepository
|
||||
->findByDateAndEmployeesIndexedByEmployeeId($workDate, array_values($employeesById))
|
||||
;
|
||||
$absenceByEmployeeId = [];
|
||||
foreach ($this->absenceRepository->findByDateAndEmployees($workDate, array_values($employeesById)) as $absence) {
|
||||
$absenceEmployeeId = $absence->getEmployee()?->getId();
|
||||
if ($absenceEmployeeId) {
|
||||
$absenceByEmployeeId[$absenceEmployeeId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$result = new WorkHourBulkUpsertResult();
|
||||
|
||||
@@ -77,10 +86,18 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
|
||||
throw new AccessDeniedHttpException(sprintf('Employee %d is outside your scope.', $employeeId));
|
||||
}
|
||||
|
||||
$contract = $this->contractResolver->resolveForEmployeeAndDate($employee, $workDate);
|
||||
$contract = $this->contractResolver->resolveForEmployeeAndDate($employee, $workDate);
|
||||
if (null === $contract) {
|
||||
throw new UnprocessableEntityHttpException(sprintf(
|
||||
'Employee %d has no active contract on %s.',
|
||||
$employeeId,
|
||||
$data->workDate
|
||||
));
|
||||
}
|
||||
$isPresenceTracking = TrackingMode::PRESENCE->value === $contract?->getTrackingMode();
|
||||
$normalized = $this->normalizeEntry($entry, $employeeId, $isPresenceTracking);
|
||||
$existing = $existingByEmployeeId[$employeeId] ?? null;
|
||||
$isAdmin = in_array('ROLE_ADMIN', $user->getRoles(), true);
|
||||
|
||||
if ($existing?->isValid()) {
|
||||
if (!$this->isSameAsExisting($existing, $normalized)) {
|
||||
@@ -95,11 +112,34 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$isAdmin && $existing?->isSiteValid()) {
|
||||
if (!$this->isSameAsExisting($existing, $normalized)) {
|
||||
throw new UnprocessableEntityHttpException(sprintf(
|
||||
'Employee %d: site validated work hour cannot be modified.',
|
||||
$employeeId
|
||||
));
|
||||
}
|
||||
|
||||
++$result->processed;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->isEntryEmpty($normalized)) {
|
||||
// Convention choisie: une ligne vide supprime l'enregistrement existant.
|
||||
if ($existing) {
|
||||
$this->entityManager->remove($existing);
|
||||
++$result->deleted;
|
||||
} elseif (($absenceByEmployeeId[$employeeId] ?? false) === true) {
|
||||
// Si une absence existe ce jour, on garde une ligne technique pour pouvoir valider la journée.
|
||||
$workHour = new WorkHour()
|
||||
->setEmployee($employee)
|
||||
->setWorkDate($workDate)
|
||||
;
|
||||
$this->hydrateWorkHour($workHour, $normalized);
|
||||
$this->entityManager->persist($workHour);
|
||||
$existingByEmployeeId[$employeeId] = $workHour;
|
||||
++$result->created;
|
||||
}
|
||||
|
||||
++$result->processed;
|
||||
@@ -187,14 +227,16 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
|
||||
}
|
||||
|
||||
return [
|
||||
'morningFrom' => $this->normalizeTime($entry['morningFrom'] ?? null, $employeeId, 'morningFrom'),
|
||||
'morningTo' => $this->normalizeTime($entry['morningTo'] ?? null, $employeeId, 'morningTo'),
|
||||
'afternoonFrom' => $this->normalizeTime($entry['afternoonFrom'] ?? null, $employeeId, 'afternoonFrom'),
|
||||
'afternoonTo' => $this->normalizeTime($entry['afternoonTo'] ?? null, $employeeId, 'afternoonTo'),
|
||||
'eveningFrom' => $this->normalizeTime($entry['eveningFrom'] ?? null, $employeeId, 'eveningFrom'),
|
||||
'eveningTo' => $this->normalizeTime($entry['eveningTo'] ?? null, $employeeId, 'eveningTo'),
|
||||
'isPresentMorning' => false,
|
||||
'isPresentAfternoon' => false,
|
||||
'morningFrom' => $this->normalizeTime($entry['morningFrom'] ?? null, $employeeId, 'morningFrom'),
|
||||
'morningTo' => $this->normalizeTime($entry['morningTo'] ?? null, $employeeId, 'morningTo'),
|
||||
'afternoonFrom' => $this->normalizeTime($entry['afternoonFrom'] ?? null, $employeeId, 'afternoonFrom'),
|
||||
'afternoonTo' => $this->normalizeTime($entry['afternoonTo'] ?? null, $employeeId, 'afternoonTo'),
|
||||
'eveningFrom' => $this->normalizeTime($entry['eveningFrom'] ?? null, $employeeId, 'eveningFrom'),
|
||||
'eveningTo' => $this->normalizeTime($entry['eveningTo'] ?? null, $employeeId, 'eveningTo'),
|
||||
// On conserve aussi la présence si envoyée (cas forfait affiché côté UI),
|
||||
// même si le contrat résolu ce jour est en suivi horaire.
|
||||
'isPresentMorning' => $this->normalizePresence($entry['isPresentMorning'] ?? false, $employeeId, 'isPresentMorning'),
|
||||
'isPresentAfternoon' => $this->normalizePresence($entry['isPresentAfternoon'] ?? false, $employeeId, 'isPresentAfternoon'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -284,6 +326,8 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
|
||||
->setEveningTo($entry['eveningTo'])
|
||||
->setIsPresentMorning($entry['isPresentMorning'])
|
||||
->setIsPresentAfternoon($entry['isPresentAfternoon'])
|
||||
// Toute modification invalide la validation chef de site.
|
||||
->setIsSiteValid(false)
|
||||
// Toute modification utilisateur repasse la ligne en attente de validation RH.
|
||||
->setIsValid(false)
|
||||
;
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Dto\WorkHours\DayContextRow;
|
||||
use App\Entity\User;
|
||||
use App\Repository\Contract\AbsenceReadRepositoryInterface;
|
||||
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
|
||||
use App\Service\Contracts\EmployeeContractResolver;
|
||||
use App\Service\WorkHours\AbsenceSegmentsResolver;
|
||||
use App\Service\WorkHours\WorkedHoursCreditPolicy;
|
||||
use DateTimeImmutable;
|
||||
@@ -26,6 +27,7 @@ final readonly class WorkHourDayContextProvider implements ProviderInterface
|
||||
private RequestStack $requestStack,
|
||||
private EmployeeScopedRepositoryInterface $employeeRepository,
|
||||
private AbsenceReadRepositoryInterface $absenceRepository,
|
||||
private EmployeeContractResolver $contractResolver,
|
||||
private AbsenceSegmentsResolver $absenceSegmentsResolver,
|
||||
private WorkedHoursCreditPolicy $workedHoursCreditPolicy,
|
||||
) {}
|
||||
@@ -50,7 +52,10 @@ final readonly class WorkHourDayContextProvider implements ProviderInterface
|
||||
}
|
||||
|
||||
// On initialise toutes les lignes, même sans absence ce jour-là.
|
||||
$rowsByEmployeeId[$employeeId] = new DayContextRow(employeeId: $employeeId);
|
||||
$rowsByEmployeeId[$employeeId] = new DayContextRow(
|
||||
employeeId: $employeeId,
|
||||
hasContractAtDate: null !== $this->contractResolver->resolveForEmployeeAndDate($employee, $workDate)
|
||||
);
|
||||
}
|
||||
|
||||
$dateKey = $workDate->format('Y-m-d');
|
||||
|
||||
54
src/State/WorkHourSiteValidationProcessor.php
Normal file
54
src/State/WorkHourSiteValidationProcessor.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Entity\User;
|
||||
use App\Entity\WorkHour;
|
||||
use App\Security\EmployeeScopeService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
final readonly class WorkHourSiteValidationProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private EmployeeScopeService $employeeScopeService,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): WorkHour
|
||||
{
|
||||
if (!$data instanceof WorkHour) {
|
||||
throw new AccessDeniedHttpException('Invalid payload.');
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('Authentication required.');
|
||||
}
|
||||
|
||||
// Réservé aux profils "Sites" (ni admin, ni self).
|
||||
if (in_array('ROLE_ADMIN', $user->getRoles(), true) || in_array('ROLE_SELF', $user->getRoles(), true)) {
|
||||
throw new AccessDeniedHttpException('Only site managers can update site validation.');
|
||||
}
|
||||
|
||||
$siteId = $data->getEmployee()?->getSite()?->getId();
|
||||
if (!$siteId) {
|
||||
throw new AccessDeniedHttpException('Employee site is required.');
|
||||
}
|
||||
|
||||
$allowedSiteIds = $this->employeeScopeService->getAllowedSiteIds($user);
|
||||
if (!in_array($siteId, $allowedSiteIds, true)) {
|
||||
throw new AccessDeniedHttpException('Employee is outside your site scope.');
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user