diff --git a/CLAUDE.md b/CLAUDE.md index cd33261..635947a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,6 +48,12 @@ - Saisie d'heures (ou de jours de présence) autorisée sur un férié - **Crédit automatique des heures contractuelles** sur un férié Lun-Ven pour tout contrat hors Forfait, **uniquement en l'absence d'absence déclarée** : le total journalier = `max(saisie + credited_absence, référence_contractuelle)`. Référence : 35h→7h, 39h→8h Lun-Jeu/7h Ven, CUSTOM→weeklyHours/5, INTERIM→idem 35h/39h/custom selon weeklyHours. Aucune ligne BDD créée (crédit virtuel). Drivers : crédité en `dayHoursMinutes`. Impacte directement le total hebdo RTT (tranches 25%/50%). Dès qu'une absence est posée sur le férié, le crédit virtuel saute — c'est le `countAsWorkedHours` du type d'absence qui pilote. Services : `App\Service\WorkHours\HolidayVirtualHoursResolver` + `DailyReferenceMinutesResolver`. Doc complète : `doc/holiday-virtual-hours.md`. +## Commentaires de semaine +- Entité `EmployeeWeekComment` : commentaire libre par employé et semaine ISO (unique `(employee_id, week_start_date)`). `week_start_date` = lundi. +- CRUD `/employee_week_comments` `ROLE_ADMIN`. Write processor audite via `AuditLogger`. +- Picto bulle vue semaine (HoursWeekView + DriverHoursWeekView) : fond bleu/rouge. Intégré dans `WeeklySummaryRow.comment/commentId`. +- Doc : `doc/week-comments.md`. + ## Validation Rules - `isValid` (RH): locks line for everyone (admin can only untoggle validation) - `isSiteValid` (site manager): locks for non-admin, admin can still edit diff --git a/frontend/components/driver-hours/DriverHoursWeekView.vue b/frontend/components/driver-hours/DriverHoursWeekView.vue index a750128..0c4999b 100644 --- a/frontend/components/driver-hours/DriverHoursWeekView.vue +++ b/frontend/components/driver-hours/DriverHoursWeekView.vue @@ -33,8 +33,11 @@ {{ row.firstName }} {{ row.lastName }} ({{ row.contractName ?? '-' }})

-

- {{ row.siteName ?? 'Sans site' }} — {{ contractNatureLabel(row.contractNature) }} +

+ {{ row.siteName ?? 'Sans site' }} — {{ contractNatureLabel(row.contractNature) }} +

@@ -118,9 +121,12 @@ const cellTitle = (daily: { defineProps<{ isWeekLoading: boolean + isAdmin: boolean weekGridCols: string weeklySummary: WeeklyWorkHourSummary | null weekDayHeaders: Array<{ date: string; weekday: string; dayDate: string }> formatMinutes: (minutes: number) => string }>() + +defineEmits<{ (e: 'open-comment', row: WeeklyWorkHourSummary['rows'][number]): void }>() diff --git a/frontend/components/hours/HoursWeekView.vue b/frontend/components/hours/HoursWeekView.vue index d9033dc..32ffdc3 100644 --- a/frontend/components/hours/HoursWeekView.vue +++ b/frontend/components/hours/HoursWeekView.vue @@ -94,8 +94,11 @@ {{ row.firstName }} {{ row.lastName }} ({{ row.contractName ?? '-' }})

-

- {{ row.siteName ?? 'Sans site' }} — {{ contractNatureLabel(row.contractNature) }} +

+ {{ row.siteName ?? 'Sans site' }} — {{ contractNatureLabel(row.contractNature) }} +

@@ -179,9 +182,12 @@ const cellTitle = (daily: { defineProps<{ isWeekLoading: boolean + isAdmin: boolean weekGridCols: string weeklySummary: WeeklyWorkHourSummary | null weekDayHeaders: Array<{ date: string; label: string }> formatMinutes: (minutes: number) => string }>() + +defineEmits<{ (e: 'open-comment', row: WeeklyWorkHourSummary['rows'][number]): void }>() diff --git a/frontend/components/hours/WeekCommentDrawer.vue b/frontend/components/hours/WeekCommentDrawer.vue new file mode 100644 index 0000000..c9adedf --- /dev/null +++ b/frontend/components/hours/WeekCommentDrawer.vue @@ -0,0 +1,81 @@ + + + diff --git a/frontend/composables/useDriverHoursPage.ts b/frontend/composables/useDriverHoursPage.ts index 74fd5fb..92510d7 100644 --- a/frontend/composables/useDriverHoursPage.ts +++ b/frontend/composables/useDriverHoursPage.ts @@ -926,6 +926,15 @@ export const useDriverHoursPage = () => { } } + const isWeekCommentDrawerOpen = ref(false) + const weekCommentContext = ref<{ employeeId: number; employeeLabel: string; weekStart: string; weekEnd: string; content: string; commentId: number | null } | null>(null) + const openWeekCommentDrawer = (row: { employeeId: number; firstName: string; lastName: string; comment?: string | null; commentId?: number | null }) => { + if (!weeklySummary.value) return + weekCommentContext.value = { employeeId: row.employeeId, employeeLabel: `${row.firstName} ${row.lastName}`.trim(), weekStart: weeklySummary.value.weekStart, weekEnd: weeklySummary.value.weekEnd, content: row.comment ?? '', commentId: row.commentId ?? null } + isWeekCommentDrawerOpen.value = true + } + const reloadWeeklySummary = async () => { await loadWeeklySummary() } + return { isAdmin, isSelfUser, @@ -993,6 +1002,10 @@ export const useDriverHoursPage = () => { deleteAbsenceFromDrawer, closeAbsenceDrawer, formatMinutes, - handleSave + handleSave, + isWeekCommentDrawerOpen, + weekCommentContext, + openWeekCommentDrawer, + reloadWeeklySummary } } diff --git a/frontend/composables/useHoursPage.ts b/frontend/composables/useHoursPage.ts index 76860ed..c59f541 100644 --- a/frontend/composables/useHoursPage.ts +++ b/frontend/composables/useHoursPage.ts @@ -1112,6 +1112,15 @@ export const useHoursPage = () => { } } + const isWeekCommentDrawerOpen = ref(false) + const weekCommentContext = ref<{ employeeId: number; employeeLabel: string; weekStart: string; weekEnd: string; content: string; commentId: number | null } | null>(null) + const openWeekCommentDrawer = (row: { employeeId: number; firstName: string; lastName: string; comment?: string | null; commentId?: number | null }) => { + if (!weeklySummary.value) return + weekCommentContext.value = { employeeId: row.employeeId, employeeLabel: `${row.firstName} ${row.lastName}`.trim(), weekStart: weeklySummary.value.weekStart, weekEnd: weeklySummary.value.weekEnd, content: row.comment ?? '', commentId: row.commentId ?? null } + isWeekCommentDrawerOpen.value = true + } + const reloadWeeklySummary = async () => { await loadWeeklySummary() } + return { isAdmin, isSelfUser, @@ -1186,6 +1195,10 @@ export const useHoursPage = () => { deleteAbsenceFromDrawer, closeAbsenceDrawer, formatMinutes, - handleSave + handleSave, + isWeekCommentDrawerOpen, + weekCommentContext, + openWeekCommentDrawer, + reloadWeeklySummary } } diff --git a/frontend/data/documentation-content.ts b/frontend/data/documentation-content.ts index 42d405a..a1c1e24 100644 --- a/frontend/data/documentation-content.ts +++ b/frontend/data/documentation-content.ts @@ -80,6 +80,16 @@ export const documentationSections: DocSection[] = [ { type: 'list', content: 'Jour : total des heures dans la plage 06:00–21:00\nNuit : total des heures dans les plages 00:00–06:00 et 21:00–24:00\nTotal : somme des heures de jour et de nuit' }, ], }, + { + id: 'commentaire-semaine', + title: 'Commentaires de semaine (admin)', + requiredLevel: 'admin', + blocks: [ + { type: 'paragraph', content: 'Sur la vue semaine, un picto bulle permet d\'attacher un commentaire libre sur la semaine d\'un employé.' }, + { type: 'list', content: 'Bulle bleue : pas de commentaire\nBulle rouge : un commentaire existe\nClic : ouvre le drawer avec textarea' }, + { type: 'note', content: 'Les commentaires n\'affectent aucun calcul. Pour supprimer, videz la textarea puis Enregistrer, ou bouton Supprimer.' }, + ], + }, ], }, { diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index 75abe0e..4d2fb3e 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -59,6 +59,10 @@ }, "leaveRecap": { "load": "Impossible de charger le récap des congés." + }, + "weekComment": { + "save": "Impossible d'enregistrer le commentaire de semaine.", + "delete": "Impossible de supprimer le commentaire de semaine." } }, "success": { @@ -110,6 +114,10 @@ "create": "Observation créée.", "update": "Observation mise à jour.", "delete": "Observation supprimée." + }, + "weekComment": { + "save": "Commentaire enregistré.", + "delete": "Commentaire supprimé." } } } diff --git a/frontend/pages/driver-hours.vue b/frontend/pages/driver-hours.vue index 5b74cf8..dae82bc 100644 --- a/frontend/pages/driver-hours.vue +++ b/frontend/pages/driver-hours.vue @@ -74,11 +74,13 @@ @@ -110,6 +112,17 @@ @cancel="closeAbsenceDrawer" /> + @@ -179,7 +192,11 @@ const { formatMinutes, isSelectedDateHoliday, selectedHolidayLabel, - handleSave + handleSave, + isWeekCommentDrawerOpen, + weekCommentContext, + openWeekCommentDrawer, + reloadWeeklySummary } = useDriverHoursPage() useHead({ diff --git a/frontend/pages/hours.vue b/frontend/pages/hours.vue index debc600..ed5456b 100644 --- a/frontend/pages/hours.vue +++ b/frontend/pages/hours.vue @@ -81,11 +81,13 @@ @@ -117,6 +119,17 @@ @cancel="closeAbsenceDrawer" /> + @@ -193,7 +206,11 @@ const { deleteAbsenceFromDrawer, closeAbsenceDrawer, formatMinutes, - handleSave + handleSave, + isWeekCommentDrawerOpen, + weekCommentContext, + openWeekCommentDrawer, + reloadWeeklySummary } = useHoursPage() useHead({ diff --git a/frontend/services/dto/work-hour.ts b/frontend/services/dto/work-hour.ts index 64d0b5b..03e6a41 100644 --- a/frontend/services/dto/work-hour.ts +++ b/frontend/services/dto/work-hour.ts @@ -89,6 +89,8 @@ export type WeeklyWorkHourRowSummary = { weeklyOvernightCount?: number hasContractForWeek?: boolean contractNature?: 'CDI' | 'CDD' | 'INTERIM' | null + comment?: string | null + commentId?: number | null } export type WeeklyWorkHourSummary = { diff --git a/frontend/services/employee-week-comments.ts b/frontend/services/employee-week-comments.ts new file mode 100644 index 0000000..dd1e745 --- /dev/null +++ b/frontend/services/employee-week-comments.ts @@ -0,0 +1,24 @@ +export type EmployeeWeekComment = { + id: number + weekStartDate: string + content: string +} + +export const createWeekComment = async (payload: { employeeId: number; weekStartDate: string; content: string }) => { + const api = useApi() + return api.post('/employee_week_comments', { + employee: `/api/employees/${payload.employeeId}`, + weekStartDate: payload.weekStartDate, + content: payload.content + }, { toastSuccessKey: 'success.weekComment.save', toastErrorKey: 'errors.weekComment.save' }) +} + +export const updateWeekComment = async (id: number, content: string) => { + const api = useApi() + return api.patch(`/employee_week_comments/${id}`, { content }, { toastSuccessKey: 'success.weekComment.save', toastErrorKey: 'errors.weekComment.save' }) +} + +export const deleteWeekComment = async (id: number) => { + const api = useApi() + await api.delete(`/employee_week_comments/${id}`, {}, { toastSuccessKey: 'success.weekComment.delete', toastErrorKey: 'errors.weekComment.delete' }) +} diff --git a/migrations/Version20260417100000.php b/migrations/Version20260417100000.php new file mode 100644 index 0000000..3d958b1 --- /dev/null +++ b/migrations/Version20260417100000.php @@ -0,0 +1,29 @@ +addSql('CREATE TABLE employee_week_comments (id SERIAL NOT NULL, employee_id INT NOT NULL, week_start_date DATE NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX uniq_employee_week_comment ON employee_week_comments (employee_id, week_start_date)'); + $this->addSql('CREATE INDEX idx_ewc_week_start ON employee_week_comments (week_start_date)'); + $this->addSql('ALTER TABLE employee_week_comments ADD CONSTRAINT fk_ewc_employee FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP TABLE employee_week_comments'); + } +} diff --git a/src/Dto/WorkHours/WeeklySummaryRow.php b/src/Dto/WorkHours/WeeklySummaryRow.php index e4aacc4..812f5bf 100644 --- a/src/Dto/WorkHours/WeeklySummaryRow.php +++ b/src/Dto/WorkHours/WeeklySummaryRow.php @@ -35,5 +35,7 @@ final class WeeklySummaryRow public int $weeklyOvernightCount = 0, public bool $hasContractForWeek = true, public ?string $contractNature = null, + public ?string $comment = null, + public ?int $commentId = null, ) {} } diff --git a/src/Entity/EmployeeWeekComment.php b/src/Entity/EmployeeWeekComment.php new file mode 100644 index 0000000..762e4f4 --- /dev/null +++ b/src/Entity/EmployeeWeekComment.php @@ -0,0 +1,136 @@ + ['week_comment:read'], 'datetime_format' => 'Y-m-d'], + denormalizationContext: ['groups' => ['week_comment:write'], 'datetime_format' => 'Y-m-d'], + order: ['weekStartDate' => 'DESC'], + paginationEnabled: false, +)] +#[ApiFilter(DateFilter::class, properties: ['weekStartDate'])] +#[ApiFilter(SearchFilter::class, properties: ['employee' => 'exact'])] +#[ORM\Entity(repositoryClass: EmployeeWeekCommentRepository::class)] +#[ORM\Table(name: 'employee_week_comments')] +#[ORM\UniqueConstraint(name: 'uniq_employee_week_comment', columns: ['employee_id', 'week_start_date'])] +class EmployeeWeekComment +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + #[Groups(['week_comment:read'])] + private ?int $id = null; + + #[ORM\ManyToOne(targetEntity: Employee::class)] + #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] + #[Groups(['week_comment:read', 'week_comment:write'])] + #[Assert\NotNull] + private ?Employee $employee = null; + + #[ORM\Column(type: 'date_immutable')] + #[Groups(['week_comment:read', 'week_comment:write'])] + #[Assert\NotNull] + private ?DateTimeImmutable $weekStartDate = null; + + #[ORM\Column(type: 'text')] + #[Groups(['week_comment:read', 'week_comment:write'])] + #[Assert\NotBlank] + #[Assert\Length(max: 5000)] + private string $content = ''; + + #[ORM\Column(type: 'datetime_immutable')] + #[Groups(['week_comment:read'])] + private DateTimeImmutable $createdAt; + + #[ORM\Column(type: 'datetime_immutable')] + #[Groups(['week_comment:read'])] + private DateTimeImmutable $updatedAt; + + public function __construct() + { + $now = new DateTimeImmutable(); + $this->createdAt = $now; + $this->updatedAt = $now; + } + + 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 getWeekStartDate(): ?DateTimeImmutable + { + return $this->weekStartDate; + } + + public function setWeekStartDate(?DateTimeImmutable $weekStartDate): self + { + $this->weekStartDate = $weekStartDate; + + return $this; + } + + public function getContent(): string + { + return $this->content; + } + + public function setContent(string $content): self + { + $this->content = $content; + + return $this; + } + + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): DateTimeImmutable + { + return $this->updatedAt; + } + + public function touchUpdatedAt(): void + { + $this->updatedAt = new DateTimeImmutable(); + } +} diff --git a/src/Repository/EmployeeWeekCommentRepository.php b/src/Repository/EmployeeWeekCommentRepository.php new file mode 100644 index 0000000..9984a8d --- /dev/null +++ b/src/Repository/EmployeeWeekCommentRepository.php @@ -0,0 +1,58 @@ + + */ +class EmployeeWeekCommentRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, EmployeeWeekComment::class); + } + + public function findOneByEmployeeAndWeek(Employee $employee, DateTimeImmutable $weekStart): ?EmployeeWeekComment + { + return $this->findOneBy(['employee' => $employee, 'weekStartDate' => $weekStart]); + } + + /** + * @param list $employees + * + * @return array employee_id → comment + */ + public function findByWeekAndEmployees(DateTimeImmutable $weekStart, array $employees): array + { + if ([] === $employees) { + return []; + } + + $rows = $this->createQueryBuilder('c') + ->andWhere('c.weekStartDate = :weekStart') + ->andWhere('c.employee IN (:employees)') + ->setParameter('weekStart', $weekStart) + ->setParameter('employees', $employees) + ->innerJoin('c.employee', 'e')->addSelect('e') + ->getQuery()->getResult() + ; + + $map = []; + foreach ($rows as $row) { + $eid = $row->getEmployee()?->getId(); + if (null !== $eid) { + $map[$eid] = $row; + } + } + + return $map; + } +} diff --git a/src/State/EmployeeWeekCommentWriteProcessor.php b/src/State/EmployeeWeekCommentWriteProcessor.php new file mode 100644 index 0000000..766d8a9 --- /dev/null +++ b/src/State/EmployeeWeekCommentWriteProcessor.php @@ -0,0 +1,80 @@ +getEmployee(); + + if ($operation instanceof DeleteOperationInterface) { + $this->auditLogger->log( + $employee, + 'delete', + 'week_comment', + $data->getId(), + sprintf('Commentaire semaine supprimé pour %s (semaine du %s)', $this->label($employee), $data->getWeekStartDate()?->format('d/m/Y') ?? '?'), + ['old' => ['content' => $data->getContent()]], + $data->getWeekStartDate(), + ); + $result = $this->removeProcessor->process($data, $operation, $uriVariables, $context); + $this->entityManager->flush(); + + return $result; + } + + $weekStart = $data->getWeekStartDate(); + if (null === $weekStart || '1' !== $weekStart->format('N')) { + throw new UnprocessableEntityHttpException('weekStartDate must be a Monday (ISO weekday 1).'); + } + + $prev = null; + if (null !== $data->getId()) { + $prev = $this->entityManager->getUnitOfWork()->getOriginalEntityData($data)['content'] ?? null; + $data->touchUpdatedAt(); + } + + $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context); + + if (null === $prev) { + $this->auditLogger->log($employee, 'create', 'week_comment', $data->getId(), sprintf('Commentaire semaine créé pour %s (semaine du %s)', $this->label($employee), $weekStart->format('d/m/Y')), ['new' => ['content' => $data->getContent()]], $weekStart); + } elseif ($prev !== $data->getContent()) { + $this->auditLogger->log($employee, 'update', 'week_comment', $data->getId(), sprintf('Commentaire semaine modifié pour %s (semaine du %s)', $this->label($employee), $weekStart->format('d/m/Y')), ['old' => ['content' => $prev], 'new' => ['content' => $data->getContent()]], $weekStart); + } + + $this->entityManager->flush(); + + return $result; + } + + private function label(mixed $e): string + { + return $e instanceof Employee ? trim(($e->getLastName() ?? '').' '.($e->getFirstName() ?? '')) : '?'; + } +} diff --git a/src/State/WorkHourWeeklySummaryProvider.php b/src/State/WorkHourWeeklySummaryProvider.php index 41a196f..00ae3f1 100644 --- a/src/State/WorkHourWeeklySummaryProvider.php +++ b/src/State/WorkHourWeeklySummaryProvider.php @@ -13,6 +13,7 @@ use App\Dto\WorkHours\WorkMetrics; use App\Entity\Absence; use App\Entity\Contract; use App\Entity\Employee; +use App\Entity\EmployeeWeekComment; use App\Entity\User; use App\Entity\WorkHour; use App\Enum\ContractNature; @@ -21,6 +22,7 @@ use App\Enum\TrackingMode; use App\Repository\Contract\AbsenceReadRepositoryInterface; use App\Repository\Contract\EmployeeScopedRepositoryInterface; use App\Repository\Contract\WorkHourReadRepositoryInterface; +use App\Repository\EmployeeWeekCommentRepository; use App\Service\Contracts\EmployeeContractResolver; use App\Service\PublicHolidayServiceInterface; use App\Service\WorkHours\AbsenceSegmentsResolver; @@ -48,6 +50,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface private DailyReferenceMinutesResolver $dailyReferenceResolver, private HolidayVirtualHoursResolver $holidayVirtualHoursResolver, private PublicHolidayServiceInterface $publicHolidayService, + private EmployeeWeekCommentRepository $weekCommentRepository, ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): WorkHourWeeklySummary @@ -65,11 +68,13 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface $workHours = $this->workHourRepository->findByDateRangeAndEmployees($weekStart, $weekEnd, $employees); $absences = $this->absenceRepository->findForPrint($weekStart, $weekEnd, $employees); + $weekComments = $this->weekCommentRepository->findByWeekAndEmployees($weekStart, $employees); + $summary = new WorkHourWeeklySummary(); $summary->weekStart = $weekStart->format('Y-m-d'); $summary->weekEnd = $weekEnd->format('Y-m-d'); $summary->days = $days; - $summary->rows = $this->buildRows($employees, $workHours, $absences, $days, $anchorDate->format('Y-m-d')); + $summary->rows = $this->buildRows($employees, $workHours, $absences, $days, $anchorDate->format('Y-m-d'), $weekComments); return $summary; } @@ -112,14 +117,15 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface } /** - * @param list $employees - * @param list $workHours - * @param list $absences - * @param list $days + * @param list $employees + * @param list $workHours + * @param list $absences + * @param list $days + * @param array $weekComments * * @return list */ - private function buildRows(array $employees, array $workHours, array $absences, array $days, string $anchorDateYmd): array + private function buildRows(array $employees, array $workHours, array $absences, array $days, string $anchorDateYmd, array $weekComments = []): array { $contractsByEmployeeDate = $this->contractResolver->resolveForEmployeesAndDays($employees, $days); $contractNaturesByEmployeeDate = $this->contractResolver->resolveNaturesForEmployeesAndDays($employees, $days); @@ -375,6 +381,8 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface weeklyOvernightCount: $weeklyOvernightCount, hasContractForWeek: $hasContractForWeek, contractNature: $weekAnchorContractNature->value, + comment: ($weekComments[$employeeId] ?? null)?->getContent(), + commentId: ($weekComments[$employeeId] ?? null)?->getId(), ); } diff --git a/tests/State/EmployeeWeekCommentWriteProcessorTest.php b/tests/State/EmployeeWeekCommentWriteProcessorTest.php new file mode 100644 index 0000000..0e31796 --- /dev/null +++ b/tests/State/EmployeeWeekCommentWriteProcessorTest.php @@ -0,0 +1,76 @@ +createStub(ProcessorInterface::class), + $this->createStub(ProcessorInterface::class), + $this->createStub(EntityManagerInterface::class), + $this->createStub(AuditLogger::class), + ); + + $comment = new EmployeeWeekComment() + ->setEmployee(new Employee()->setFirstName('A')->setLastName('B')) + ->setWeekStartDate(new DateTimeImmutable('2026-04-14')) + ->setContent('test') + ; + + $this->expectException(UnprocessableEntityHttpException::class); + $processor->process($comment, new Post()); + } + + public function testAcceptsMondayAndAuditsCreate(): void + { + $persist = $this->createMock(ProcessorInterface::class); + $persist->expects(self::once())->method('process'); + $em = $this->createMock(EntityManagerInterface::class); + $em->method('getUnitOfWork')->willReturn($this->createStub(UnitOfWork::class)); + $em->expects(self::once())->method('flush'); + $auditor = $this->createMock(AuditLogger::class); + $auditor->expects(self::once())->method('log')->with(self::anything(), 'create', 'week_comment'); + + $processor = new EmployeeWeekCommentWriteProcessor($persist, $this->createStub(ProcessorInterface::class), $em, $auditor); + $processor->process( + new EmployeeWeekComment()->setEmployee(new Employee()->setFirstName('A')->setLastName('B'))->setWeekStartDate(new DateTimeImmutable('2026-04-13'))->setContent('x'), + new Post() + ); + } + + public function testDeleteAudits(): void + { + $remove = $this->createMock(ProcessorInterface::class); + $remove->expects(self::once())->method('process'); + $em = $this->createMock(EntityManagerInterface::class); + $em->expects(self::once())->method('flush'); + $auditor = $this->createMock(AuditLogger::class); + $auditor->expects(self::once())->method('log')->with(self::anything(), 'delete', 'week_comment'); + + $processor = new EmployeeWeekCommentWriteProcessor($this->createStub(ProcessorInterface::class), $remove, $em, $auditor); + $processor->process( + new EmployeeWeekComment()->setEmployee(new Employee()->setFirstName('A')->setLastName('B'))->setWeekStartDate(new DateTimeImmutable('2026-04-13'))->setContent('x'), + new Delete() + ); + } +} diff --git a/tests/State/WorkHourWeeklySummaryProviderTest.php b/tests/State/WorkHourWeeklySummaryProviderTest.php index 8f53d81..63ba1de 100644 --- a/tests/State/WorkHourWeeklySummaryProviderTest.php +++ b/tests/State/WorkHourWeeklySummaryProviderTest.php @@ -15,6 +15,7 @@ use App\Enum\HalfDay; use App\Repository\Contract\AbsenceReadRepositoryInterface; use App\Repository\Contract\EmployeeScopedRepositoryInterface; use App\Repository\Contract\WorkHourReadRepositoryInterface; +use App\Repository\EmployeeWeekCommentRepository; use App\Service\Contracts\EmployeeContractResolver; use App\Service\PublicHolidayServiceInterface; use App\Service\WorkHours\AbsenceSegmentsResolver; @@ -67,6 +68,7 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase new DailyReferenceMinutesResolver(), $this->buildHolidayResolver(), $this->buildHolidayService(), + $this->buildWeekCommentRepoStub(), ); $this->expectException(AccessDeniedHttpException::class); @@ -130,6 +132,7 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase new DailyReferenceMinutesResolver(), $this->buildHolidayResolver(), $this->buildHolidayService(), + $this->buildWeekCommentRepoStub(), ); $result = $provider->provide(new Get()); @@ -180,6 +183,14 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase $property->setValue($entity, $id); } + private function buildWeekCommentRepoStub(): EmployeeWeekCommentRepository + { + $r = $this->createStub(EmployeeWeekCommentRepository::class); + $r->method('findByWeekAndEmployees')->willReturn([]); + + return $r; + } + private function buildHolidayResolver(array $holidayMap = []): HolidayVirtualHoursResolver { return new HolidayVirtualHoursResolver(