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
+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 {listEmployees, updateEmployeeOrder} from '~/services/employees'
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 type {Formation} from '~/services/dto/formation'
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 CalendarGrid from '~/components/CalendarGrid.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.")
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) => {
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 aEnd = normalizeDate(absence.endDate)
if (start > aEnd || end < aStart) return false
@@ -701,28 +760,15 @@ const handleSubmit = async () => {
}
}
if (editingAbsence.value) {
await updateAbsence({
id: editingAbsence.value.id,
employeeId: Number(form.employeeId),
typeId: Number(form.typeId),
startDate: form.startDate,
startHalf: form.startHalf,
endDate: form.endDate,
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
})
}
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()
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 () => {
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
await deleteAbsence(editingAbsence.value.id)
for (const absence of toDelete) {
await deleteAbsence(absence.id)
}
closeDrawer()
await loadAbsences()
}