feat(calendar) : suppression et modification d'une plage de congés d'un coup

Sur le calendrier, une absence est stockée une ligne par jour sans lien
entre les jours. La suppression et la modification n'agissaient donc que
sur le jour cliqué.

- Supprimer (handleDelete) : efface toutes les absences de l'employé
  comprises dans la plage [début ; fin] du drawer (jours sans absence
  ignorés, jour validé protégé côté backend).
- Modifier (handleSubmit) : remplacement de bloc — supprime l'ancien bloc
  contigu de même type (vers l'avant depuis le jour cliqué) + les absences
  recouvertes par la nouvelle plage, puis recrée la plage. Corrige le bug
  du PATCH qui laissait des jours fantômes (raccourcissement) et des
  doublons (allongement). updateAbsence n'est plus utilisé sur le calendrier.

Backend AbsenceWriteProcessor non touché : les écrans Heures verrouillent
les dates du drawer, le PATCH y reste mono-jour.

Doc : functional-rules.md, documentation-content.ts (in-app), CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 14:23:05 +02:00
parent 0333270089
commit 4aeff28af4
4 changed files with 119 additions and 28 deletions
+4
View File
@@ -39,6 +39,10 @@
- **Calendrier des jours validés (vue Jour)** (`WorkHourValidationStatusProvider`, ressource `WorkHourValidationStatus`, endpoint `GET /work-hours/validation-status?from=&to=[&driver=1]`, `ROLE_USER`) : en **vue Jour**, sur les **deux écrans** (Heures **et** Heures Conducteurs), le sélecteur de date est un `MalioDate` (layer `@malio/layer-ui >= 1.7.x` : prop `markedDates` + event `@month-change`) qui peint **en vert** (`markedDates``'success'`) les jours **entièrement validés**. **Définition** : un jour est vert ssi il porte ≥1 ligne `WorkHour` du scope ce jour-là **et** aucune n'est `isValid=false` — on se base sur la **seule** colonne `is_valid` (validation admin ; `isSiteValid` ignoré). Jour **sans aucune ligne** → neutre (jamais vert). **Périmètre complet** via `EmployeeRepository::findScoped` (admin = tous sites, chef de site = ses sites), **indépendant du filtre sites** de l'écran. **Scope conducteur inversé** par `?driver=1` : écran Heures → non-conducteurs (défaut), écran Heures Conducteurs → conducteurs (résolu par date via `EmployeeContractResolver::resolveIsDriverForEmployeeAndDate`, mémoïsé ; garde `if ($isDriver !== $driverOnly) continue`). Provider : une requête `WorkHourReadRepositoryInterface::findByDateRangeAndEmployees`, agrégation par jour (`total`/`pending`), plage bornée à 366 j. **Chargement à la volée par mois** (jamais préchargé) : `@month-change {month,year}` (à l'ouverture + nav) → fetch de la **grille visible** (lundi avant le 1er → dimanche après le dernier) → cache `validatedDaysByMonth` (`useHoursPage` / `useDriverHoursPage`, ce dernier passe `{ driver: true }` au service) → `markedDates` réactif. **Rafraîchissement** du mois en cache (`reloadValidationMonth`) après `toggleValidation`/`toggleValidationBulk`/`handleSave`/`refreshAfterAbsenceChange` (pas la validation site). La **vue Semaine** utilise un `MalioDateWeek` (sélecteur de semaine, v-model ISO week `YYYY-Www`, sans coloration). Le **stepper du mode jour est remplacé** par le `MalioDate` (raccourcis Hier/Aujourd'hui/Demain conservés) ; `PeriodStepperPicker` reste un fallback de la vue Jour quand `showValidationCalendar` est absent (aucun appelant actuel). Activation par écran via la prop `showValidationCalendar` de `HoursToolbar` (les deux pages la passent à `true` + `markedDates` + `@month-change`). Alignement vertical de la ligne via `lg:items-center` (les champs Malio font `h-12` vs `h-10` des boutons). Doc complète : `doc/hours-validated-days.md`. - **Calendrier des jours validés (vue Jour)** (`WorkHourValidationStatusProvider`, ressource `WorkHourValidationStatus`, endpoint `GET /work-hours/validation-status?from=&to=[&driver=1]`, `ROLE_USER`) : en **vue Jour**, sur les **deux écrans** (Heures **et** Heures Conducteurs), le sélecteur de date est un `MalioDate` (layer `@malio/layer-ui >= 1.7.x` : prop `markedDates` + event `@month-change`) qui peint **en vert** (`markedDates``'success'`) les jours **entièrement validés**. **Définition** : un jour est vert ssi il porte ≥1 ligne `WorkHour` du scope ce jour-là **et** aucune n'est `isValid=false` — on se base sur la **seule** colonne `is_valid` (validation admin ; `isSiteValid` ignoré). Jour **sans aucune ligne** → neutre (jamais vert). **Périmètre complet** via `EmployeeRepository::findScoped` (admin = tous sites, chef de site = ses sites), **indépendant du filtre sites** de l'écran. **Scope conducteur inversé** par `?driver=1` : écran Heures → non-conducteurs (défaut), écran Heures Conducteurs → conducteurs (résolu par date via `EmployeeContractResolver::resolveIsDriverForEmployeeAndDate`, mémoïsé ; garde `if ($isDriver !== $driverOnly) continue`). Provider : une requête `WorkHourReadRepositoryInterface::findByDateRangeAndEmployees`, agrégation par jour (`total`/`pending`), plage bornée à 366 j. **Chargement à la volée par mois** (jamais préchargé) : `@month-change {month,year}` (à l'ouverture + nav) → fetch de la **grille visible** (lundi avant le 1er → dimanche après le dernier) → cache `validatedDaysByMonth` (`useHoursPage` / `useDriverHoursPage`, ce dernier passe `{ driver: true }` au service) → `markedDates` réactif. **Rafraîchissement** du mois en cache (`reloadValidationMonth`) après `toggleValidation`/`toggleValidationBulk`/`handleSave`/`refreshAfterAbsenceChange` (pas la validation site). La **vue Semaine** utilise un `MalioDateWeek` (sélecteur de semaine, v-model ISO week `YYYY-Www`, sans coloration). Le **stepper du mode jour est remplacé** par le `MalioDate` (raccourcis Hier/Aujourd'hui/Demain conservés) ; `PeriodStepperPicker` reste un fallback de la vue Jour quand `showValidationCalendar` est absent (aucun appelant actuel). Activation par écran via la prop `showValidationCalendar` de `HoursToolbar` (les deux pages la passent à `true` + `markedDates` + `@month-change`). Alignement vertical de la ligne via `lg:items-center` (les champs Malio font `h-12` vs `h-10` des boutons). Doc complète : `doc/hours-validated-days.md`.
- **Export Contingent heures de nuit** (`NightHoursContingentPrintProvider`, endpoint `GET /night-hours-contingent/print?year=YYYY`, `ROLE_USER`) : option « Contingent H.nuit » du drawer Export de la liste employés. PDF **A4 paysage**, lignes = employés **groupés par site** et triés `displayOrder`/nom/prénom (comme le day-export), colonnes = 12 mois civils, chacun avec 2 sous-colonnes **H.nuit** et **N.jours**. Heures de nuit = minutes dans la fenêtre **21h→6h** via le service partagé `App\Service\WorkHours\NightHoursCalculator` (source unique mutualisée avec `WorkHourWeeklySummaryProvider`, `YearlyHoursExportBuilder`, `RttRecoveryComputationService` et `SalaryRecapPrintProvider`). Conducteurs inclus via `WorkHour.nightHoursMinutes`. **N.jours** = nb de jours où minutes de nuit ≥ 240 (4h). **Aucun crédit** absence/férié. Agrégation : `App\Service\WorkHours\NightContingentExportBuilder`. Gabarit `templates/night-hours-contingent/print.html.twig`. - **Export Contingent heures de nuit** (`NightHoursContingentPrintProvider`, endpoint `GET /night-hours-contingent/print?year=YYYY`, `ROLE_USER`) : option « Contingent H.nuit » du drawer Export de la liste employés. PDF **A4 paysage**, lignes = employés **groupés par site** et triés `displayOrder`/nom/prénom (comme le day-export), colonnes = 12 mois civils, chacun avec 2 sous-colonnes **H.nuit** et **N.jours**. Heures de nuit = minutes dans la fenêtre **21h→6h** via le service partagé `App\Service\WorkHours\NightHoursCalculator` (source unique mutualisée avec `WorkHourWeeklySummaryProvider`, `YearlyHoursExportBuilder`, `RttRecoveryComputationService` et `SalaryRecapPrintProvider`). Conducteurs inclus via `WorkHour.nightHoursMinutes`. **N.jours** = nb de jours où minutes de nuit ≥ 240 (4h). **Aucun crédit** absence/férié. Agrégation : `App\Service\WorkHours\NightContingentExportBuilder`. Gabarit `templates/night-hours-contingent/print.html.twig`.
- **Écran Calendrier** : un employé est affiché uniquement si au moins une de ses périodes de contrat (`employee.contractHistory`) intersecte le mois affiché (`[1er ; dernier jour]`). Filtre côté frontend dans `visibleEmployees` (`pages/calendar.vue`). **L'impression PDF des absences applique le même filtre** côté backend (`AbsencePrintProvider::hasContractInRange` sur la période `[from, to]`) : un salarié parti en avril n'apparaît pas sur une impression de mai. **Le récap salaire applique le même filtre** (`SalaryRecapPrintProvider::hasContractInRange` sur le mois imprimé) : un salarié sans contrat sur le mois (ex. parti en février) n'apparaît pas sur le récap de juin. - **Écran Calendrier** : un employé est affiché uniquement si au moins une de ses périodes de contrat (`employee.contractHistory`) intersecte le mois affiché (`[1er ; dernier jour]`). Filtre côté frontend dans `visibleEmployees` (`pages/calendar.vue`). **L'impression PDF des absences applique le même filtre** côté backend (`AbsencePrintProvider::hasContractInRange` sur la période `[from, to]`) : un salarié parti en avril n'apparaît pas sur une impression de mai. **Le récap salaire applique le même filtre** (`SalaryRecapPrintProvider::hasContractInRange` sur le mois imprimé) : un salarié sans contrat sur le mois (ex. parti en février) n'apparaît pas sur le récap de juin.
- **Suppression / modification d'une plage de congés** (`calendar.vue`) : une absence = **une ligne par jour** en BDD (aucun lien entre les jours, cf. `expandAbsenceRange`), donc tout se gère côté frontend qui a la plage visible.
- **Supprimer** (`handleDelete`) : efface **toutes les absences de l'employé dont le jour tombe dans `[form.startDate ; form.endDate]`** (filtrage sur `absences.value`, boucle `deleteAbsence`), pas seulement le jour cliqué. Flux RH : clic sur un jour → drawer (début = fin) → étendre la date de fin → Supprimer. Jours sans absence ignorés ; jour validé (`isValid`/site) bloque sa propre ligne (backend). Confirmation avec nombre de jours + intervalle (`formatYmdToFr`) dès > 1 jour.
- **Modifier** (`handleSubmit`, branche `editingAbsence`) : **remplacement de bloc** — supprime l'**ancien bloc contigu** (jours adjacents de **même type**, en partant du jour cliqué **vers l'avant** via `shiftYmd`) **+** toute absence recouverte par la nouvelle plage, puis **`createAbsence`** sur la nouvelle plage (plus de `PATCH`). Corrige le bug historique : raccourcir ne laisse plus de **jours fantômes**, ré-étendre ne crée plus de **doublons**. Jamais de modification des jours **antérieurs** au jour cliqué ; confirmation « chevauche une autre » seulement si on écrase un **autre type**. La branche **Création** garde sa détection de chevauchement demi-journée (`getSegmentsForDate`).
- Backend `AbsenceWriteProcessor` (PATCH) **non touché** : il reste mono-jour en pratique car les écrans Heures/Heures Conducteurs verrouillent les dates du drawer (`lock-dates`), seul le calendrier reshape une plage. `updateAbsence` n'est plus appelé depuis `calendar.vue` (import retiré).
- **Planning jours travaillés** (`EmployeeContractPeriod.workDaysHours` : JSON `{iso_day: minutes}`) : obligatoire pour tout contrat TIME **hors 35h/39h/INTERIM** (ex. 4h, 25h, 28h). Somme = `weeklyHours × 60`. Utilisé par `HolidayVirtualHoursResolver` (crédit férié) et `WorkedHoursCreditPolicy` (crédit absence) pour ne créditer que les jours effectivement travaillés. Validation : `EmployeeContractPeriodValidator::assertWorkDaysHours`. - **Planning jours travaillés** (`EmployeeContractPeriod.workDaysHours` : JSON `{iso_day: minutes}`) : obligatoire pour tout contrat TIME **hors 35h/39h/INTERIM** (ex. 4h, 25h, 28h). Somme = `weeklyHours × 60`. Utilisé par `HolidayVirtualHoursResolver` (crédit férié) et `WorkedHoursCreditPolicy` (crédit absence) pour ne créditer que les jours effectivement travaillés. Validation : `EmployeeContractPeriodValidator::assertWorkDaysHours`.
- Absences: stored per day (auto-split), AM/PM/full day, clear corresponding hour slots - Absences: stored per day (auto-split), AM/PM/full day, clear corresponding hour slots
- Absences with `countAsWorkedHours=true`: credit minutes (TIME) or nothing (PRESENCE) - Absences with `countAsWorkedHours=true`: credit minutes (TIME) or nothing (PRESENCE)
+11
View File
@@ -78,6 +78,17 @@ Documents complementaires:
- Calendrier congés: fond coloré selon la couleur du type d'absence (`AbsenceType.color`) - Calendrier congés: fond coloré selon la couleur du type d'absence (`AbsenceType.color`)
- demi-journée: dégradé diagonal - demi-journée: dégradé diagonal
- journée complète: fond plein - journée complète: fond plein
- Suppression d'une plage depuis le Calendrier:
- clic sur un jour d'une plage → le drawer s'ouvre sur ce jour (début = fin = jour cliqué)
- on étend la **date de fin** (ou de début) pour couvrir la plage à effacer, puis bouton **Supprimer**
- **toutes** les absences de l'employé dont le jour tombe dans la plage sélectionnée sont supprimées (1 ligne/jour en BDD)
- les jours de la plage sans absence sont ignorés (aucune erreur) ; un jour validé (`isValid`/site) bloque sa propre suppression
- confirmation unique avant suppression ; au-delà de 1 jour le message rappelle le nombre de jours et l'intervalle
- Modification d'une plage depuis le Calendrier (bouton **Modifier**):
- une absence n'a **aucun lien** entre ses jours en BDD (1 ligne/jour). Modifier réalise donc un **remplacement de bloc** : on supprime l'ancien **bloc contigu** (jours adjacents de **même type**, en partant du jour cliqué **vers l'avant**) puis on **recrée** la nouvelle plage
- corrige le bug historique du PATCH : raccourcir une plage ne laisse plus de **jours fantômes** au-delà de la nouvelle fin, et ré-étendre ne crée plus de **doublons**
- les jours **antérieurs** au jour cliqué ne sont jamais touchés ; toute absence d'un autre type recouverte par la nouvelle plage déclenche une confirmation « chevauche une autre »
- implémenté côté frontend (`calendar.vue::handleSubmit`) car le backend ne peut pas reconstituer le bloc sans identifiant de groupe ; sans danger sur les écrans Heures/Heures Conducteurs où les dates du drawer sont verrouillées (`lock-dates`), donc le PATCH y reste mono-jour
- Visibilité des employés dans le Calendrier: - Visibilité des employés dans le Calendrier:
- un employé est affiché si au moins une de ses périodes de contrat intersecte le mois affiché - un employé est affiché si au moins une de ses périodes de contrat intersecte le mois affiché
- un employé dont toutes les périodes se terminent avant le 1er du mois (ou commencent après la fin du mois) est masqué - un employé dont toutes les périodes se terminent avant le 1er du mois (ou commencent après la fin du mois) est masqué
+2
View File
@@ -416,6 +416,8 @@ export const documentationSections: DocSection[] = [
{ type: 'paragraph', content: 'Les absences peuvent être posées depuis la vue jour des heures ou depuis le calendrier.' }, { type: 'paragraph', content: 'Les absences peuvent être posées depuis la vue jour des heures ou depuis le calendrier.' },
{ type: 'list', content: 'Journée complète : efface toutes les plages horaires\nDemi-journée matin (AM) : efface le créneau matin\nDemi-journée après-midi (PM) : efface les créneaux après-midi et soir' }, { type: 'list', content: 'Journée complète : efface toutes les plages horaires\nDemi-journée matin (AM) : efface le créneau matin\nDemi-journée après-midi (PM) : efface les créneaux après-midi et soir' },
{ type: 'paragraph', content: 'Les absences sont stockées par jour : une absence de plusieurs jours est automatiquement découpée en entrées quotidiennes.' }, { type: 'paragraph', content: 'Les absences sont stockées par jour : une absence de plusieurs jours est automatiquement découpée en entrées quotidiennes.' },
{ type: 'note', content: 'Supprimer une plage depuis le calendrier : cliquez sur un jour de la plage (le drawer s\'ouvre sur ce jour), étendez la date de fin (ou de début) pour couvrir toute la plage à effacer, puis cliquez sur « Supprimer ». Tous les jours de congé compris dans la plage sélectionnée sont supprimés en une fois ; les jours sans absence dans cette plage sont simplement ignorés. Un jour déjà validé reste protégé.' },
{ type: 'note', content: 'Modifier une plage depuis le calendrier : cliquez sur le premier jour de la plage, ajustez la date de fin (pour la raccourcir ou l\'allonger) puis « Modifier ». La plage est remplacée proprement par la nouvelle : plus de jours « fantômes » qui restaient après un raccourcissement, ni de doublons après un allongement. Les jours situés avant le jour cliqué ne sont jamais modifiés.' },
], ],
}, },
{ {
+102 -28
View File
@@ -109,11 +109,11 @@ import type {HalfDay} from '~/services/dto/half-day'
import {HALF_DAYS} from '~/services/dto/half-day' import {HALF_DAYS} from '~/services/dto/half-day'
import {listEmployees, updateEmployeeOrder} from '~/services/employees' import {listEmployees, updateEmployeeOrder} from '~/services/employees'
import {listAbsenceTypes} from '~/services/absence-types' import {listAbsenceTypes} from '~/services/absence-types'
import {createAbsence, deleteAbsence, listAbsences, updateAbsence} from '~/services/absences' import {createAbsence, deleteAbsence, listAbsences} from '~/services/absences'
import {listFormationsByDateRange} from '~/services/formations' import {listFormationsByDateRange} from '~/services/formations'
import type {Formation} from '~/services/dto/formation' import type {Formation} from '~/services/dto/formation'
import {listPublicHolidays} from '~/services/public-holidays' import {listPublicHolidays} from '~/services/public-holidays'
import {getDaysInMonth, normalizeDate, parseYmd, toYmd} from '~/utils/date' import {formatYmdToFr, getDaysInMonth, normalizeDate, parseYmd, shiftYmd, toYmd} from '~/utils/date'
import {compareEmployeesInSite, sortEmployeesBySiteAndOrder} from '~/utils/employee' import {compareEmployeesInSite, sortEmployeesBySiteAndOrder} from '~/utils/employee'
import CalendarGrid from '~/components/CalendarGrid.vue' import CalendarGrid from '~/components/CalendarGrid.vue'
import AbsenceFormDrawer from '~/components/AbsenceFormDrawer.vue' import AbsenceFormDrawer from '~/components/AbsenceFormDrawer.vue'
@@ -649,9 +649,68 @@ const handleSubmit = async () => {
window.alert("La demi-journee de fin ne peut pas etre avant la demi-journee de debut.") window.alert("La demi-journee de fin ne peut pas etre avant la demi-journee de debut.")
return return
} }
if (editingAbsence.value) {
// Modification d'une plage : une absence = une ligne par jour, sans lien en BDD.
// On remplace donc tout le bloc contigu (même type) partant du jour cliqué par la
// nouvelle plage : suppression de l'ancien bloc + recréation. Évite les jours
// fantômes (raccourcissement) et les doublons (l'ancien PATCH ne nettoyait rien).
const originalEmployeeId = editingAbsence.value.employee.id
const newEmployeeId = Number(form.employeeId)
const originalTypeId = editingAbsence.value.type.id
const clickedDate = normalizeDate(editingAbsence.value.startDate)
// Bloc contigu (vers l'avant) depuis le jour cliqué, même employé + même type d'origine.
// On ne touche jamais aux jours antérieurs au jour cliqué.
const sameLeaveDays = new Set(
absences.value
.filter((absence) => absence.employee?.id === originalEmployeeId && absence.type?.id === originalTypeId)
.map((absence) => normalizeDate(absence.startDate))
)
const blockDates = new Set<string>()
let cursor: string | null = clickedDate
while (cursor && sameLeaveDays.has(cursor)) {
blockDates.add(cursor)
cursor = shiftYmd(cursor, 1)
}
// À supprimer : l'ancien bloc + toute absence recouverte par la nouvelle plage.
const toReplace = absences.value.filter((absence) => {
const day = normalizeDate(absence.startDate)
const inBlock = absence.employee?.id === originalEmployeeId && blockDates.has(day)
const inNewRange = absence.employee?.id === newEmployeeId && day >= start && day <= end
return inBlock || inNewRange
})
// Confirmation uniquement si on écrase une absence d'un AUTRE type (vrai chevauchement).
const replacesForeign = toReplace.some((absence) => absence.type?.id !== originalTypeId)
if (replacesForeign) {
const confirmReplace = window.confirm(
"Cette absence chevauche une autre. Voulez-vous la remplacer ?"
)
if (!confirmReplace) return
}
for (const absence of toReplace) {
await deleteAbsence(absence.id)
}
await createAbsence({
employeeId: newEmployeeId,
typeId: Number(form.typeId),
startDate: form.startDate,
startHalf: form.startHalf,
endDate: form.endDate,
endHalf: form.endHalf,
comment: form.comment
})
closeDrawer()
await loadAbsences()
return
}
// Création : détection de chevauchement (précision demi-journée) puis remplacement.
const overlaps = absences.value.filter((absence) => { const overlaps = absences.value.filter((absence) => {
if (absence.employee?.id !== Number(form.employeeId)) return false if (absence.employee?.id !== Number(form.employeeId)) return false
if (editingAbsence.value && absence.id === editingAbsence.value.id) return false
const aStart = normalizeDate(absence.startDate) const aStart = normalizeDate(absence.startDate)
const aEnd = normalizeDate(absence.endDate) const aEnd = normalizeDate(absence.endDate)
if (start > aEnd || end < aStart) return false if (start > aEnd || end < aStart) return false
@@ -701,28 +760,15 @@ const handleSubmit = async () => {
} }
} }
if (editingAbsence.value) { await createAbsence({
await updateAbsence({ employeeId: Number(form.employeeId),
id: editingAbsence.value.id, typeId: Number(form.typeId),
employeeId: Number(form.employeeId), startDate: form.startDate,
typeId: Number(form.typeId), startHalf: form.startHalf,
startDate: form.startDate, endDate: form.endDate,
startHalf: form.startHalf, endHalf: form.endHalf,
endDate: form.endDate, comment: form.comment
endHalf: form.endHalf, })
comment: form.comment
})
} else {
await createAbsence({
employeeId: Number(form.employeeId),
typeId: Number(form.typeId),
startDate: form.startDate,
startHalf: form.startHalf,
endDate: form.endDate,
endHalf: form.endHalf,
comment: form.comment
})
}
closeDrawer() closeDrawer()
await loadAbsences() await loadAbsences()
@@ -731,14 +777,42 @@ const handleSubmit = async () => {
} }
} }
// Suppression de l'absence en cours d'édition. // Suppression: efface toutes les absences de l'employé comprises dans la plage
// sélectionnée (date début → date fin du drawer). Comme une absence = une ligne
// par jour en BDD, on supprime chaque jour existant de la plage ; les jours sans
// absence (ex. une date hors plage réelle) sont naturellement ignorés.
const handleDelete = async () => { const handleDelete = async () => {
if (!editingAbsence.value) return if (!editingAbsence.value) return
const confirmDelete = window.confirm('Supprimer cette absence ?') const employeeId = editingAbsence.value.employee.id
const rangeStart = normalizeDate(form.startDate)
const rangeEnd = normalizeDate(form.endDate)
if (rangeStart > rangeEnd) {
window.alert("La date de fin ne peut pas etre avant la date de debut.")
return
}
const toDelete = absences.value.filter((absence) => {
if (absence.employee?.id !== employeeId) return false
const day = normalizeDate(absence.startDate)
return day >= rangeStart && day <= rangeEnd
})
if (toDelete.length === 0) {
closeDrawer()
return
}
const confirmDelete = window.confirm(
toDelete.length === 1
? 'Supprimer cette absence ?'
: `Supprimer ${toDelete.length} jours de congé du ${formatYmdToFr(rangeStart)} au ${formatYmdToFr(rangeEnd)} ?`
)
if (!confirmDelete) return if (!confirmDelete) return
await deleteAbsence(editingAbsence.value.id) for (const absence of toDelete) {
await deleteAbsence(absence.id)
}
closeDrawer() closeDrawer()
await loadAbsences() await loadAbsences()
} }