From 1e691786b3e02dd1775be4f1f32fa92e60e330a6 Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 25 Mar 2026 08:50:33 +0100 Subject: [PATCH] =?UTF-8?q?feat=20:=20Export=20des=20heures=20d'un=20emplo?= =?UTF-8?q?y=C3=A9=20sur=20une=20ann=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/sqldialects.xml | 6 - doc/functional-rules.md | 35 +- .../components/EmployeeYearlyHoursDrawer.vue | 67 +++ frontend/composables/useEmployeeContract.ts | 52 ++- frontend/pages/employees/[id].vue | 31 +- src/ApiResource/EmployeeYearlyHoursPrint.php | 25 ++ ...eContractPeriodReadRepositoryInterface.php | 2 + .../EmployeeContractPeriodRepository.php | 12 + src/State/EmployeeWriteProcessor.php | 9 +- .../EmployeeYearlyHoursPrintProvider.php | 420 ++++++++++++++++++ .../employee-yearly-hours/print.html.twig | 151 +++++++ 11 files changed, 777 insertions(+), 33 deletions(-) delete mode 100644 .idea/sqldialects.xml create mode 100644 frontend/components/EmployeeYearlyHoursDrawer.vue create mode 100644 src/ApiResource/EmployeeYearlyHoursPrint.php create mode 100644 src/State/EmployeeYearlyHoursPrintProvider.php create mode 100644 templates/employee-yearly-hours/print.html.twig diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml deleted file mode 100644 index 3fadc3d..0000000 --- a/.idea/sqldialects.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/doc/functional-rules.md b/doc/functional-rules.md index 19fd2ff..bdda7fc 100644 --- a/doc/functional-rules.md +++ b/doc/functional-rules.md @@ -194,13 +194,14 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer. - Détail employé: - onglet `Suivi contrat` avec affichage de l'historique des périodes de contrat - chaque ligne expose: nature (`CDI`/`CDD`/`INTERIM`), contrat/temps de travail, date de début, date de fin (ou "En cours") - - action `Clôturer`: - - bouton actif uniquement s'il existe un contrat en cours non déjà clôturé à la date du jour + - action `Modifier` (clôture/solde de tout compte): + - bouton actif s'il existe un contrat en cours non clôturé, ou si le dernier contrat est terminé (sans contrat actif après) - ouvre un drawer en lecture seule (type/temps de travail/date de début) - champs saisissables: - - `contractEndDate` (prérempli à aujourd'hui) + - `contractEndDate` (prérempli à aujourd'hui si contrat en cours, à la date de fin existante si contrat terminé) - `contractPaidLeaveSettled` (checkbox "Soldé dans le solde de tout compte") - backend: en mode clôture, le flag `contractPaidLeaveSettled` est persisté sur la période clôturée + - cas du contrat déjà terminé: permet de modifier `paidLeaveSettled` et le commentaire sur le dernier contrat terminé (ex: solde de tout compte CDD) - action `Ajouter`: - conserve le flux d'ajout d'un nouveau contrat via drawer dédié - disponible uniquement s'il n'y a pas de contrat en cours, ou si le contrat en cours a déjà une date de fin @@ -384,3 +385,31 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer. - Une notification est créée uniquement quand un chef de site termine la validation complète: - condition: plus aucune ligne `work_hours` du site à la date concernée avec `isSiteValid = false` - destinataires: utilisateurs `ROLE_ADMIN` + +## 16) Export PDF des heures annuelles + +- Accessible depuis la fiche employé (bouton imprimante à droite du nom) +- Ouvre un drawer pour choisir l'année (civile, Jan-Déc) +- Génère un PDF avec le détail jour par jour des heures de l'employé +- Seuls les jours avec heures saisies ou absence sont affichés + +### Colonnes selon le mode de suivi + +- **TIME (non-chauffeur)**: Date | Absence | Début matin | Fin matin | Début après-midi | Fin après-midi | Début soir | Fin soir | Total +- **PRESENCE (forfait)**: Date | Absence | Présence matin | Présence après-midi | Total +- **Chauffeur**: Date | Absence | Heures jour | Heures nuit | Heures atelier | Total + +### Changement de contrat en cours d'année + +- Si l'employé change de mode de suivi (TIME/PRESENCE) ou de statut chauffeur en cours d'année, le PDF affiche des sections séparées avec les colonnes adaptées à chaque période +- Le nom du contrat est affiché en sous-titre de chaque section + +### Calcul du total + +- TIME non-chauffeur: somme des créneaux matin + après-midi + soir, plus minutes créditées des absences `countAsWorkedHours` +- Chauffeur: `dayHoursMinutes + nightHoursMinutes + workshopHoursMinutes` + minutes créditées +- PRESENCE: 0.5 par demi-journée présente (matin/après-midi), max 1.0 + +### Nom du fichier + +- Format: `{nom}_{prenom}_{annee}.pdf` diff --git a/frontend/components/EmployeeYearlyHoursDrawer.vue b/frontend/components/EmployeeYearlyHoursDrawer.vue new file mode 100644 index 0000000..d043daf --- /dev/null +++ b/frontend/components/EmployeeYearlyHoursDrawer.vue @@ -0,0 +1,67 @@ + + + diff --git a/frontend/composables/useEmployeeContract.ts b/frontend/composables/useEmployeeContract.ts index 85929af..d3f5d4d 100644 --- a/frontend/composables/useEmployeeContract.ts +++ b/frontend/composables/useEmployeeContract.ts @@ -71,6 +71,17 @@ export const useEmployeeContract = (employee: Ref, reloadEmploy return history.find((item) => item.startDate <= today && (!item.endDate || item.endDate >= today)) ?? null }) + const lastEndedContractPeriod = computed(() => { + if (currentActiveContractPeriod.value) return null + const today = getTodayYmd() + const history = employee.value?.contractHistory ?? [] + const ended = history.filter((item) => item.endDate && item.endDate < today) + if (ended.length === 0) return null + return ended.reduce((latest, item) => (item.endDate! > latest.endDate! ? item : latest)) + }) + + const editableContractPeriod = computed(() => currentActiveContractPeriod.value ?? lastEndedContractPeriod.value) + const currentActiveContractPeriodId = computed(() => { const period = currentActiveContractPeriod.value return period?.periodId ?? null @@ -78,13 +89,15 @@ export const useEmployeeContract = (employee: Ref, reloadEmploy const canCloseCurrentContract = computed(() => { const active = currentActiveContractPeriod.value - if (!active) return false - if (!active.endDate) return true - return active.endDate > getTodayYmd() + if (active) { + if (!active.endDate) return true + return active.endDate > getTodayYmd() + } + return !!lastEndedContractPeriod.value }) const canCreateContract = computed(() => { - const active = currentActiveContractPeriod.value + const active = editableContractPeriod.value if (!active) return true return !!active.endDate }) @@ -135,15 +148,15 @@ export const useEmployeeContract = (employee: Ref, reloadEmploy const hydrateContractFormFromCurrent = () => { const current = employee.value - const active = currentActiveContractPeriod.value - if (!current || !active) return + const period = editableContractPeriod.value + if (!current || !period) return - contractForm.contractId = active.contractId ?? current.contract?.id ?? '' - contractForm.contractName = active.contractName ?? current.contract?.name ?? '' - contractForm.weeklyHours = active.weeklyHours ?? current.contract?.weeklyHours ?? null - contractForm.contractNature = active.contractNature - contractForm.startDate = active.startDate - contractForm.endDate = getTodayYmd() + contractForm.contractId = period.contractId ?? current.contract?.id ?? '' + contractForm.contractName = period.contractName ?? current.contract?.name ?? '' + contractForm.weeklyHours = period.weeklyHours ?? current.contract?.weeklyHours ?? null + contractForm.contractNature = period.contractNature + contractForm.startDate = period.startDate + contractForm.endDate = period.endDate ?? getTodayYmd() contractForm.paidLeaveSettled = false contractForm.comment = '' } @@ -173,8 +186,8 @@ export const useEmployeeContract = (employee: Ref, reloadEmploy createContractForm.contractNature = 'CDI' createContractForm.endDate = '' createContractForm.isDriver = false - createContractForm.startDate = currentActiveContractPeriod.value?.endDate - ? (shiftYmd(currentActiveContractPeriod.value.endDate, 1) ?? currentActiveContractPeriod.value.endDate) + createContractForm.startDate = editableContractPeriod.value?.endDate + ? (shiftYmd(editableContractPeriod.value.endDate, 1) ?? editableContractPeriod.value.endDate) : getTodayYmd() resetCreateValidation() isCreateContractDrawerOpen.value = true @@ -185,15 +198,16 @@ export const useEmployeeContract = (employee: Ref, reloadEmploy } const submitContractUpdate = async () => { - if (!employee.value || isContractSubmitting.value || !currentActiveContractPeriod.value) return + const period = editableContractPeriod.value + if (!employee.value || isContractSubmitting.value || !period) return validationTouched.endDate = true if (!isContractEndDateValid.value) return - if (contractForm.endDate < currentActiveContractPeriod.value.startDate) { + if (contractForm.endDate < period.startDate) { toast.error({ title: 'Erreur', - message: `La date de fin doit être postérieure au ${formatDate(currentActiveContractPeriod.value.startDate)}.` + message: `La date de fin doit être postérieure au ${formatDate(period.startDate)}.` }) return } @@ -226,8 +240,8 @@ export const useEmployeeContract = (employee: Ref, reloadEmploy createValidationTouched.endDate = true if (!isCreateContractFormValid.value) return - if (currentActiveContractPeriod.value?.endDate) { - const minStartDate = shiftYmd(currentActiveContractPeriod.value.endDate, 1) ?? currentActiveContractPeriod.value.endDate + if (editableContractPeriod.value?.endDate) { + const minStartDate = shiftYmd(editableContractPeriod.value.endDate, 1) ?? editableContractPeriod.value.endDate if (createContractForm.startDate < minStartDate) { toast.error({ title: 'Erreur', diff --git a/frontend/pages/employees/[id].vue b/frontend/pages/employees/[id].vue index 9adc269..e5cb294 100644 --- a/frontend/pages/employees/[id].vue +++ b/frontend/pages/employees/[id].vue @@ -13,7 +13,16 @@
-

{{ employee.firstName }} {{ employee.lastName }}

+
+

{{ employee.firstName }} {{ employee.lastName }}

+ +

Date d'entrée : {{ employee.entryDate ? employee.entryDate.split('-').reverse().join('/') : '-' }}

@@ -166,10 +175,24 @@
+ +