Chargement...
@@ -275,6 +299,12 @@ const {
submitCreateMileage,
submitUpdateMileage,
submitDeleteMileage,
+ formations,
+ isFormationLoading,
+ formationApiBase,
+ submitCreateFormation,
+ submitUpdateFormation,
+ submitDeleteFormation,
bonuses,
isBonusLoading,
submitCreateBonus,
diff --git a/frontend/pages/hours.vue b/frontend/pages/hours.vue
index c4fa0ed..5068f2c 100644
--- a/frontend/pages/hours.vue
+++ b/frontend/pages/hours.vue
@@ -67,6 +67,8 @@
:get-row-metrics="getRowMetrics"
:get-row-absence-label="getRowAbsenceLabel"
:get-row-absence-style="getRowAbsenceStyle"
+ :has-row-formation="hasRowFormation"
+ :get-row-formation-label="getRowFormationLabel"
:get-row-updated-at="getRowUpdatedAt"
:get-presence-day-value="getPresenceDayValue"
:on-absence-click="openAbsenceDrawer"
@@ -177,6 +179,8 @@ const {
getRowMetrics,
getRowAbsenceLabel,
getRowAbsenceStyle,
+ hasRowFormation,
+ getRowFormationLabel,
getRowUpdatedAt,
getPresenceDayValue,
openAbsenceDrawer,
diff --git a/frontend/services/dto/formation.ts b/frontend/services/dto/formation.ts
new file mode 100644
index 0000000..56d10ee
--- /dev/null
+++ b/frontend/services/dto/formation.ts
@@ -0,0 +1,12 @@
+import type { Employee } from './employee'
+
+export type Formation = {
+ id: number
+ startDate: string
+ endDate: string
+ comment: string | null
+ justificatifPath: string | null
+ justificatifName: string | null
+ createdAt: string
+ employee?: Employee
+}
diff --git a/frontend/services/dto/work-hour.ts b/frontend/services/dto/work-hour.ts
index 9df580c..65e9fce 100644
--- a/frontend/services/dto/work-hour.ts
+++ b/frontend/services/dto/work-hour.ts
@@ -106,6 +106,8 @@ export type WorkHourDayContextRow = {
creditedMinutes: number
creditedPresenceUnits: number
isDriverContract?: boolean
+ hasFormation?: boolean
+ formationLabel?: string | null
}
export type WorkHourDayContext = {
diff --git a/frontend/services/formations.ts b/frontend/services/formations.ts
new file mode 100644
index 0000000..18ec65b
--- /dev/null
+++ b/frontend/services/formations.ts
@@ -0,0 +1,82 @@
+import { $fetch } from 'ofetch'
+import type { Formation } from './dto/formation'
+import { extractItems } from '~/utils/api'
+
+export const listFormations = async (employeeId: number) => {
+ const api = useApi()
+ const data = await api.get
(
+ '/formations',
+ { employee: `/api/employees/${employeeId}` },
+ { toast: false }
+ )
+ return extractItems(data)
+}
+
+export const listFormationsByDateRange = async (from: string, to: string) => {
+ const api = useApi()
+ const data = await api.get(
+ '/formations',
+ {
+ 'startDate[before]': to,
+ 'endDate[after]': from
+ },
+ { toast: false }
+ )
+ return extractItems(data)
+}
+
+export const createFormation = async (data: {
+ employeeId: number
+ startDate: string
+ endDate: string
+ comment?: string
+}) => {
+ const api = useApi()
+ return api.post('/formations', {
+ employee: `/api/employees/${data.employeeId}`,
+ startDate: data.startDate,
+ endDate: data.endDate,
+ comment: data.comment
+ }, {
+ toastSuccessKey: 'success.formation.create',
+ toastErrorKey: 'errors.formation.create'
+ })
+}
+
+export const updateFormation = async (id: number, data: {
+ startDate: string
+ endDate: string
+ comment?: string
+}) => {
+ const api = useApi()
+ return api.patch(`/formations/${id}`, {
+ startDate: data.startDate,
+ endDate: data.endDate,
+ comment: data.comment
+ }, {
+ toastSuccessKey: 'success.formation.update',
+ toastErrorKey: 'errors.formation.update'
+ })
+}
+
+export const deleteFormation = async (id: number) => {
+ const api = useApi()
+ return api.delete(`/formations/${id}`, {}, {
+ toastSuccessKey: 'success.formation.delete',
+ toastErrorKey: 'errors.formation.delete'
+ })
+}
+
+export const uploadFormationJustificatif = async (baseURL: string, id: number, file: File) => {
+ const formData = new FormData()
+ formData.append('file', file)
+ return $fetch(`${baseURL}/formations/${id}/justificatif`, {
+ method: 'POST',
+ body: formData,
+ credentials: 'include'
+ })
+}
+
+export const getFormationJustificatifUrl = (baseURL: string, id: number): string => {
+ return `${baseURL}/formations/${id}/justificatif`
+}
diff --git a/migrations/Version20260413120000.php b/migrations/Version20260413120000.php
new file mode 100644
index 0000000..4f0d6ef
--- /dev/null
+++ b/migrations/Version20260413120000.php
@@ -0,0 +1,29 @@
+addSql('CREATE TABLE formations (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, employee_id INT NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, comment TEXT DEFAULT NULL, justificatif_path VARCHAR(255) DEFAULT NULL, justificatif_name VARCHAR(255) DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
+ $this->addSql('CREATE INDEX IDX_FORMATION_EMPLOYEE ON formations (employee_id)');
+ $this->addSql('ALTER TABLE formations ADD CONSTRAINT FK_FORMATION_EMPLOYEE FOREIGN KEY (employee_id) REFERENCES employees (id) NOT DEFERRABLE');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE formations DROP CONSTRAINT FK_FORMATION_EMPLOYEE');
+ $this->addSql('DROP TABLE formations');
+ }
+}
diff --git a/src/Dto/WorkHours/DayContextRow.php b/src/Dto/WorkHours/DayContextRow.php
index 30ecf64..78b493c 100644
--- a/src/Dto/WorkHours/DayContextRow.php
+++ b/src/Dto/WorkHours/DayContextRow.php
@@ -17,8 +17,16 @@ final class DayContextRow
public int $creditedMinutes = 0,
public float $creditedPresenceUnits = 0.0,
public bool $isDriverContract = false,
+ public bool $hasFormation = false,
+ public ?string $formationLabel = null,
) {}
+ public function setFormation(string $label): void
+ {
+ $this->hasFormation = true;
+ $this->formationLabel = $label;
+ }
+
public function addAbsence(
?string $label,
?string $color,
@@ -64,7 +72,10 @@ final class DayContextRow
* absentMorning:bool,
* absentAfternoon:bool,
* creditedMinutes:int,
- * creditedPresenceUnits:float
+ * creditedPresenceUnits:float,
+ * isDriverContract:bool,
+ * hasFormation:bool,
+ * formationLabel:?string
* }
*/
public function toArray(): array
@@ -80,6 +91,8 @@ final class DayContextRow
'creditedMinutes' => $this->creditedMinutes,
'creditedPresenceUnits' => $this->creditedPresenceUnits,
'isDriverContract' => $this->isDriverContract,
+ 'hasFormation' => $this->hasFormation,
+ 'formationLabel' => $this->formationLabel,
];
}
diff --git a/src/Entity/Formation.php b/src/Entity/Formation.php
new file mode 100644
index 0000000..fde2a5d
--- /dev/null
+++ b/src/Entity/Formation.php
@@ -0,0 +1,192 @@
+ ['formation:read', 'employee:read'],
+ 'datetime_format' => 'Y-m-d',
+ ],
+ denormalizationContext: [
+ 'groups' => ['formation:write'],
+ 'datetime_format' => 'Y-m-d',
+ ],
+ order: ['startDate' => 'DESC'],
+ paginationEnabled: false,
+)]
+#[ApiFilter(DateFilter::class, properties: ['startDate', 'endDate'])]
+#[ApiFilter(SearchFilter::class, properties: ['employee' => 'exact'])]
+#[ORM\Entity(repositoryClass: FormationRepository::class)]
+#[ORM\Table(name: 'formations')]
+class Formation
+{
+ #[ORM\Id]
+ #[ORM\GeneratedValue]
+ #[ORM\Column(type: 'integer')]
+ #[Groups(['formation:read'])]
+ private ?int $id = null;
+
+ #[ORM\ManyToOne(targetEntity: Employee::class)]
+ #[ORM\JoinColumn(nullable: false)]
+ #[Groups(['formation:read', 'formation:write'])]
+ private ?Employee $employee = null;
+
+ #[ORM\Column(type: 'date_immutable')]
+ #[Groups(['formation:read', 'formation:write'])]
+ private ?DateTimeImmutable $startDate = null;
+
+ #[ORM\Column(type: 'date_immutable')]
+ #[Groups(['formation:read', 'formation:write'])]
+ private ?DateTimeImmutable $endDate = null;
+
+ #[ORM\Column(type: 'text', nullable: true)]
+ #[Groups(['formation:read', 'formation:write'])]
+ private ?string $comment = null;
+
+ #[ORM\Column(type: 'string', length: 255, nullable: true)]
+ #[Groups(['formation:read'])]
+ private ?string $justificatifPath = null;
+
+ #[ORM\Column(type: 'string', length: 255, nullable: true)]
+ #[Groups(['formation:read'])]
+ private ?string $justificatifName = null;
+
+ #[ORM\Column(type: 'datetime_immutable')]
+ #[Groups(['formation:read'])]
+ private DateTimeImmutable $createdAt;
+
+ public function __construct()
+ {
+ $this->createdAt = new DateTimeImmutable();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getEmployee(): ?Employee
+ {
+ return $this->employee;
+ }
+
+ public function setEmployee(?Employee $employee): self
+ {
+ $this->employee = $employee;
+
+ return $this;
+ }
+
+ public function getStartDate(): ?DateTimeImmutable
+ {
+ return $this->startDate;
+ }
+
+ public function setStartDate(?DateTimeImmutable $startDate): self
+ {
+ $this->startDate = $startDate;
+
+ return $this;
+ }
+
+ public function getEndDate(): ?DateTimeImmutable
+ {
+ return $this->endDate;
+ }
+
+ public function setEndDate(?DateTimeImmutable $endDate): self
+ {
+ $this->endDate = $endDate;
+
+ return $this;
+ }
+
+ public function getComment(): ?string
+ {
+ return $this->comment;
+ }
+
+ public function setComment(?string $comment): self
+ {
+ $this->comment = $comment;
+
+ return $this;
+ }
+
+ public function getJustificatifPath(): ?string
+ {
+ return $this->justificatifPath;
+ }
+
+ public function setJustificatifPath(?string $justificatifPath): self
+ {
+ $this->justificatifPath = $justificatifPath;
+
+ return $this;
+ }
+
+ public function getJustificatifName(): ?string
+ {
+ return $this->justificatifName;
+ }
+
+ public function setJustificatifName(?string $justificatifName): self
+ {
+ $this->justificatifName = $justificatifName;
+
+ return $this;
+ }
+
+ public function getCreatedAt(): DateTimeImmutable
+ {
+ return $this->createdAt;
+ }
+}
diff --git a/src/Repository/Contract/FormationReadRepositoryInterface.php b/src/Repository/Contract/FormationReadRepositoryInterface.php
new file mode 100644
index 0000000..5db1cb8
--- /dev/null
+++ b/src/Repository/Contract/FormationReadRepositoryInterface.php
@@ -0,0 +1,26 @@
+ $employees
+ *
+ * @return list
+ */
+ public function findByDateAndEmployees(DateTimeImmutable $date, array $employees): array;
+
+ /**
+ * @param list $employees
+ *
+ * @return list
+ */
+ public function findByDateRangeAndEmployees(DateTimeImmutable $from, DateTimeImmutable $to, array $employees): array;
+}
diff --git a/src/Repository/FormationRepository.php b/src/Repository/FormationRepository.php
new file mode 100644
index 0000000..189c7ce
--- /dev/null
+++ b/src/Repository/FormationRepository.php
@@ -0,0 +1,74 @@
+
+ */
+final class FormationRepository extends ServiceEntityRepository implements FormationReadRepositoryInterface
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, Formation::class);
+ }
+
+ /**
+ * @param list $employees
+ *
+ * @return list
+ */
+ public function findByDateAndEmployees(DateTimeImmutable $date, array $employees): array
+ {
+ if ([] === $employees) {
+ return [];
+ }
+
+ $qb = $this->createQueryBuilder('f')
+ ->leftJoin('f.employee', 'e')
+ ->addSelect('e')
+ ->andWhere('f.startDate <= :date')
+ ->andWhere('f.endDate >= :date')
+ ->andWhere('f.employee IN (:employees)')
+ ->setParameter('date', $date)
+ ->setParameter('employees', $employees)
+ ;
+
+ // @var list
+ return $qb->getQuery()->getResult();
+ }
+
+ /**
+ * @param list $employees
+ *
+ * @return list
+ */
+ public function findByDateRangeAndEmployees(DateTimeImmutable $from, DateTimeImmutable $to, array $employees): array
+ {
+ if ([] === $employees) {
+ return [];
+ }
+
+ $qb = $this->createQueryBuilder('f')
+ ->leftJoin('f.employee', 'e')
+ ->addSelect('e')
+ ->andWhere('f.startDate <= :to')
+ ->andWhere('f.endDate >= :from')
+ ->andWhere('f.employee IN (:employees)')
+ ->setParameter('from', $from)
+ ->setParameter('to', $to)
+ ->setParameter('employees', $employees)
+ ;
+
+ // @var list
+ return $qb->getQuery()->getResult();
+ }
+}
diff --git a/src/State/AbsencePrintProvider.php b/src/State/AbsencePrintProvider.php
index e2ca32d..3c81c68 100644
--- a/src/State/AbsencePrintProvider.php
+++ b/src/State/AbsencePrintProvider.php
@@ -6,9 +6,11 @@ namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
+use App\Entity\Formation;
use App\Enum\ContractNature;
use App\Enum\HalfDay;
use App\Repository\AbsenceRepository;
+use App\Repository\Contract\FormationReadRepositoryInterface;
use App\Repository\EmployeeRepository;
use App\Service\PublicHolidayServiceInterface;
use DateInterval;
@@ -30,6 +32,7 @@ class AbsencePrintProvider implements ProviderInterface
private readonly RequestStack $requestStack,
private EmployeeRepository $employeeRepository,
private AbsenceRepository $absenceRepository,
+ private FormationReadRepositoryInterface $formationRepository,
private PublicHolidayServiceInterface $publicHolidayService,
) {}
@@ -58,24 +61,27 @@ class AbsencePrintProvider implements ProviderInterface
$workContractIds = $this->parseIds($request->query->get('workContracts'));
$contractNatures = $this->parseContractNatures($request->query->get('contractNatures'));
- $employees = $this->loadEmployees($siteIds, $contractNatures, $workContractIds);
- $absences = $this->loadAbsences($fromDate, $toDate, $employees);
+ $employees = $this->loadEmployees($siteIds, $contractNatures, $workContractIds);
+ $absences = $this->loadAbsences($fromDate, $toDate, $employees);
+ $formations = $this->formationRepository->findByDateRangeAndEmployees($fromDate, $toDate, $employees);
- $days = $this->buildDays($fromDate, $toDate);
- $absenceMap = $this->buildAbsenceMap($absences, $fromDate, $toDate);
- $holidayMap = $this->buildHolidayMap($fromDate, $toDate);
+ $days = $this->buildDays($fromDate, $toDate);
+ $absenceMap = $this->buildAbsenceMap($absences, $fromDate, $toDate);
+ $formationMap = $this->buildFormationMap($formations, $fromDate, $toDate);
+ $holidayMap = $this->buildHolidayMap($fromDate, $toDate);
$options = new Options();
$options->set('isRemoteEnabled', true);
$dompdf = new Dompdf($options);
$html = $this->twig->render('absence/print.html.twig', [
- 'from' => $fromDate,
- 'to' => $toDate,
- 'days' => $days,
- 'employees' => $employees,
- 'absenceMap' => $absenceMap,
- 'holidayMap' => $holidayMap,
+ 'from' => $fromDate,
+ 'to' => $toDate,
+ 'days' => $days,
+ 'employees' => $employees,
+ 'absenceMap' => $absenceMap,
+ 'formationMap' => $formationMap,
+ 'holidayMap' => $holidayMap,
]);
$dompdf->loadHtml($html);
@@ -203,6 +209,37 @@ class AbsencePrintProvider implements ProviderInterface
return $map;
}
+ /**
+ * @param list $formations
+ *
+ * @return array>
+ */
+ private function buildFormationMap(array $formations, DateTimeImmutable $from, DateTimeImmutable $to): array
+ {
+ $map = [];
+
+ foreach ($formations as $formation) {
+ $employeeId = $formation->getEmployee()?->getId();
+ if (!$employeeId) {
+ continue;
+ }
+
+ $formationStart = DateTimeImmutable::createFromInterface($formation->getStartDate());
+ $formationEnd = DateTimeImmutable::createFromInterface($formation->getEndDate());
+
+ $start = max($formationStart, $from);
+ $end = min($formationEnd, $to);
+
+ $current = $start;
+ while ($current <= $end) {
+ $map[$employeeId][$current->format('Y-m-d')] = true;
+ $current = $current->add(new DateInterval('P1D'));
+ }
+ }
+
+ return $map;
+ }
+
private function buildHolidayMap(DateTimeImmutable $from, DateTimeImmutable $to): array
{
$map = [];
diff --git a/src/State/FormationDeleteProcessor.php b/src/State/FormationDeleteProcessor.php
new file mode 100644
index 0000000..31a5583
--- /dev/null
+++ b/src/State/FormationDeleteProcessor.php
@@ -0,0 +1,42 @@
+getJustificatifPath();
+
+ if (null !== $justificatifPath) {
+ $absolutePath = sprintf('%s/%s', $this->uploadDir, $justificatifPath);
+
+ if (file_exists($absolutePath)) {
+ unlink($absolutePath);
+ }
+ }
+
+ $this->entityManager->remove($data);
+ $this->entityManager->flush();
+
+ return null;
+ }
+}
diff --git a/src/State/FormationJustificatifDownloadProvider.php b/src/State/FormationJustificatifDownloadProvider.php
new file mode 100644
index 0000000..910065c
--- /dev/null
+++ b/src/State/FormationJustificatifDownloadProvider.php
@@ -0,0 +1,53 @@
+entityManager->find(Formation::class, $uriVariables['id']);
+
+ if (null === $formation) {
+ throw new NotFoundHttpException('Formation not found.');
+ }
+
+ $justificatifPath = $formation->getJustificatifPath();
+
+ if (null === $justificatifPath) {
+ throw new NotFoundHttpException('No justificatif found for this formation.');
+ }
+
+ $absolutePath = sprintf('%s/%s', $this->uploadDir, $justificatifPath);
+
+ if (!file_exists($absolutePath)) {
+ throw new NotFoundHttpException('Justificatif file not found.');
+ }
+
+ $response = new BinaryFileResponse($absolutePath);
+ $disposition = HeaderUtils::makeDisposition(
+ HeaderUtils::DISPOSITION_ATTACHMENT,
+ $formation->getJustificatifName() ?? 'justificatif.pdf'
+ );
+ $response->headers->set('Content-Disposition', $disposition);
+
+ return $response;
+ }
+}
diff --git a/src/State/FormationJustificatifUploadProcessor.php b/src/State/FormationJustificatifUploadProcessor.php
new file mode 100644
index 0000000..495ae8c
--- /dev/null
+++ b/src/State/FormationJustificatifUploadProcessor.php
@@ -0,0 +1,75 @@
+requestStack->getCurrentRequest();
+ $file = $request?->files->get('file');
+
+ if (null === $file) {
+ throw new BadRequestHttpException('No file uploaded.');
+ }
+
+ if ('application/pdf' !== $file->getMimeType()) {
+ throw new BadRequestHttpException('Only PDF files are accepted.');
+ }
+
+ $startDate = $data->getStartDate();
+ $year = $startDate?->format('Y') ?? date('Y');
+ $monthNumber = $startDate?->format('m') ?? date('m');
+ $relativePath = sprintf('formations/%s/%s', $year, $monthNumber);
+ $absoluteDir = sprintf('%s/%s', $this->uploadDir, $relativePath);
+
+ if (!is_dir($absoluteDir)) {
+ mkdir($absoluteDir, 0o755, true);
+ }
+
+ $filename = Uuid::v4()->toRfc4122().'.pdf';
+ $fullRelative = sprintf('%s/%s', $relativePath, $filename);
+ $originalName = $file->getClientOriginalName();
+
+ $previousPath = $data->getJustificatifPath();
+
+ $file->move($absoluteDir, $filename);
+
+ $data->setJustificatifPath($fullRelative);
+ $data->setJustificatifName($originalName);
+ $this->entityManager->flush();
+
+ if (null !== $previousPath) {
+ $previousAbsolute = sprintf('%s/%s', $this->uploadDir, $previousPath);
+ if (file_exists($previousAbsolute)) {
+ unlink($previousAbsolute);
+ }
+ }
+
+ return new JsonResponse(['path' => $fullRelative, 'name' => $originalName], Response::HTTP_OK);
+ }
+}
diff --git a/src/State/WorkHourDayContextProvider.php b/src/State/WorkHourDayContextProvider.php
index bade542..b10bf01 100644
--- a/src/State/WorkHourDayContextProvider.php
+++ b/src/State/WorkHourDayContextProvider.php
@@ -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\Repository\Contract\FormationReadRepositoryInterface;
use App\Service\Contracts\EmployeeContractResolver;
use App\Service\WorkHours\AbsenceSegmentsResolver;
use App\Service\WorkHours\WorkedHoursCreditPolicy;
@@ -27,6 +28,7 @@ final readonly class WorkHourDayContextProvider implements ProviderInterface
private RequestStack $requestStack,
private EmployeeScopedRepositoryInterface $employeeRepository,
private AbsenceReadRepositoryInterface $absenceRepository,
+ private FormationReadRepositoryInterface $formationRepository,
private EmployeeContractResolver $contractResolver,
private AbsenceSegmentsResolver $absenceSegmentsResolver,
private WorkedHoursCreditPolicy $workedHoursCreditPolicy,
@@ -40,9 +42,10 @@ final readonly class WorkHourDayContextProvider implements ProviderInterface
throw new AccessDeniedHttpException('Authentication required.');
}
- $workDate = $this->resolveWorkDate();
- $employees = $this->employeeRepository->findScoped($user);
- $absences = $this->absenceRepository->findByDateAndEmployees($workDate, $employees);
+ $workDate = $this->resolveWorkDate();
+ $employees = $this->employeeRepository->findScoped($user);
+ $absences = $this->absenceRepository->findByDateAndEmployees($workDate, $employees);
+ $formations = $this->formationRepository->findByDateAndEmployees($workDate, $employees);
$rowsByEmployeeId = [];
foreach ($employees as $employee) {
@@ -87,6 +90,14 @@ final readonly class WorkHourDayContextProvider implements ProviderInterface
);
}
+ foreach ($formations as $formation) {
+ $employeeId = $formation->getEmployee()?->getId();
+ if (!$employeeId || !isset($rowsByEmployeeId[$employeeId])) {
+ continue;
+ }
+ $rowsByEmployeeId[$employeeId]->setFormation('Formation');
+ }
+
$response = new WorkHourDayContext();
$response->workDate = $dateKey;
$response->rows = array_map(
diff --git a/templates/absence/print.html.twig b/templates/absence/print.html.twig
index 4c4b88a..25baa96 100644
--- a/templates/absence/print.html.twig
+++ b/templates/absence/print.html.twig
@@ -84,6 +84,11 @@
background: #b3e5fc;
}
+ .formation {
+ background: #6366f1;
+ color: #fff;
+ }
+
.body-cell {
height: 6mm;
padding: 0 !important;
@@ -239,11 +244,15 @@
{% for day in days %}
{% set isHoliday = holidayMap[day.date] ?? null %}
{% set info = absenceMap[employee.id][day.date] ?? null %}
+ {% set hasFormation = formationMap[employee.id][day.date] ?? false %}
+ {% set isFormationOnly = hasFormation and not info and not isHoliday %}
{% set isMonthEnd = (not loop.last) and (days[loop.index].date|date('n') != day.date|date('n')) %}
{% set isWeekend = day.date|date('N') in [6, 7] %}
-
+ |
{% if isHoliday %}
Férié
+ {% elseif isFormationOnly %}
+ F
{% elseif info %}
{% if info.half %}
{% else %}
- {{ info.code }}
+ {{ info.code }}{% if hasFormation %}*{% endif %}
{% endif %}
{% endif %}
|
diff --git a/tests/State/WorkHourDayContextProviderTest.php b/tests/State/WorkHourDayContextProviderTest.php
index 3546751..6848cd7 100644
--- a/tests/State/WorkHourDayContextProviderTest.php
+++ b/tests/State/WorkHourDayContextProviderTest.php
@@ -13,6 +13,7 @@ use App\Entity\User;
use App\Enum\HalfDay;
use App\Repository\Contract\AbsenceReadRepositoryInterface;
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
+use App\Repository\Contract\FormationReadRepositoryInterface;
use App\Service\Contracts\EmployeeContractResolver;
use App\Service\WorkHours\AbsenceSegmentsResolver;
use App\Service\WorkHours\WorkedHoursCreditPolicy;
@@ -34,14 +35,17 @@ final class WorkHourDayContextProviderTest extends TestCase
private Security $security;
private EmployeeScopedRepositoryInterface $employeeRepository;
private AbsenceReadRepositoryInterface $absenceRepository;
+ private FormationReadRepositoryInterface $formationRepository;
private RequestStack $requestStack;
protected function setUp(): void
{
- $this->security = $this->createStub(Security::class);
- $this->employeeRepository = $this->createStub(EmployeeScopedRepositoryInterface::class);
- $this->absenceRepository = $this->createStub(AbsenceReadRepositoryInterface::class);
- $this->requestStack = new RequestStack();
+ $this->security = $this->createStub(Security::class);
+ $this->employeeRepository = $this->createStub(EmployeeScopedRepositoryInterface::class);
+ $this->absenceRepository = $this->createStub(AbsenceReadRepositoryInterface::class);
+ $this->formationRepository = $this->createStub(FormationReadRepositoryInterface::class);
+ $this->formationRepository->method('findByDateAndEmployees')->willReturn([]);
+ $this->requestStack = new RequestStack();
}
public function testThrowsWhenAnonymous(): void
@@ -53,6 +57,7 @@ final class WorkHourDayContextProviderTest extends TestCase
$this->requestStack,
$this->employeeRepository,
$this->absenceRepository,
+ $this->formationRepository,
$this->buildResolverStub(),
new AbsenceSegmentsResolver(),
new WorkedHoursCreditPolicy($this->buildResolverStub())
@@ -72,6 +77,7 @@ final class WorkHourDayContextProviderTest extends TestCase
$this->requestStack,
$this->employeeRepository,
$this->absenceRepository,
+ $this->formationRepository,
$this->buildResolverStub(),
new AbsenceSegmentsResolver(),
new WorkedHoursCreditPolicy($this->buildResolverStub())
@@ -97,6 +103,7 @@ final class WorkHourDayContextProviderTest extends TestCase
$this->requestStack,
$this->employeeRepository,
$this->absenceRepository,
+ $this->formationRepository,
$this->buildResolverStub(),
new AbsenceSegmentsResolver(),
new WorkedHoursCreditPolicy($this->buildResolverStub())