Files
Lesstime/frontend/composables/useAbsenceHelpers.ts
Matthieu f9773b3a5e feat(absences) : mise en conformité légale (événements familiaux, demi-journée, CCN)
Périmètre 1-6 du design 2026-05-22-absence-legal-compliance-fixes (points
lourds — ancienneté, CP pendant maladie, rétention — reportés en backlog).

- Événements familiaux sans solde : AbsenceType::decrementsBalance() ne vaut
  true que pour les CP. Mariage/PACS, naissance, décès = droits par événement ;
  congé parental = suspension ; maladie = Sécu. Plus de solde fantôme.
- Décès : daysPerEvent = null (selon lien de parenté) + motif obligatoire à la
  création (REST + MCP), les minimums légaux étant rappelés dans l'aide.
- Ajout du congé naissance (type, policy 3 j, justificatif, libellés/couleur front).
- Garde-fou demi-journée : -0,5 appliqué uniquement si le jour-borne est
  réellement décompté (corrige un sous-décompte week-end/férié) — TDD.
- CCN documentée : paramètre app.absence.convention = "Syntec (IDCC 1486)",
  rappelée en sous-titre admin et dans l'aide /help.

Tests : AbsenceDayCalculatorTest (garde-fou demi-journée), AbsenceRequestLifecycle
(motif décès obligatoire + aucun solde touché). make test 52/52, build Nuxt OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:00:28 +02:00

95 lines
2.9 KiB
TypeScript

import type { AbsenceRequest, AbsenceStatus, AbsenceType, HalfDay } from '~/services/dto/absence'
export type BadgeVariant = 'neutral' | 'info' | 'success' | 'warning' | 'danger'
const STATUS_VARIANTS: Record<AbsenceStatus, BadgeVariant> = {
pending: 'warning',
approved: 'success',
rejected: 'danger',
cancelled: 'neutral',
}
const STATUS_ICONS: Record<AbsenceStatus, string> = {
pending: 'mdi:clock-outline',
approved: 'mdi:check-circle-outline',
rejected: 'mdi:close-circle-outline',
cancelled: 'mdi:cancel',
}
// Colours used for the calendar bars, keyed by absence type.
const TYPE_COLORS: Record<AbsenceType, string> = {
cp: '#4A90D9',
mariage_pacs: '#E91E63',
naissance: '#26A69A',
conge_parental: '#9C27B0',
deces: '#607D8B',
maladie: '#C62828',
}
export function useAbsenceHelpers() {
const { t } = useI18n()
function statusLabel(status: AbsenceStatus): string {
return t(`absences.status.${status}`)
}
function statusVariant(status: AbsenceStatus): BadgeVariant {
return STATUS_VARIANTS[status] ?? 'neutral'
}
function statusIcon(status: AbsenceStatus): string {
return STATUS_ICONS[status] ?? 'mdi:help-circle-outline'
}
function typeLabel(type: AbsenceType): string {
return t(`absences.types.${type}`)
}
function typeColor(type: AbsenceType): string {
return TYPE_COLORS[type] ?? '#9CA3AF'
}
function halfDayLabel(half: HalfDay): string {
return t(`absences.halfDay.${half}`)
}
function formatDate(iso: string | null): string {
if (!iso) return ''
const d = new Date(iso)
if (isNaN(d.getTime())) return ''
const day = String(d.getDate()).padStart(2, '0')
const month = String(d.getMonth() + 1).padStart(2, '0')
return `${day}/${month}/${d.getFullYear()}`
}
/** Human-readable period with half-day annotations. */
function formatRange(req: Pick<AbsenceRequest, 'startDate' | 'endDate' | 'startHalfDay' | 'endHalfDay'>): string {
const start = formatDate(req.startDate)
const end = formatDate(req.endDate)
const startSuffix = req.startHalfDay ? ` (${halfDayLabel(req.startHalfDay)})` : ''
const endSuffix = req.endHalfDay ? ` (${halfDayLabel(req.endHalfDay)})` : ''
if (start === end) {
return `${start}${startSuffix}`
}
return `${start}${startSuffix}${end}${endSuffix}`
}
function formatDays(days: number): string {
const rounded = Math.round(days * 2) / 2
const unit = rounded > 1 ? t('absences.daysPlural') : t('absences.daySingular')
return `${rounded} ${unit}`
}
return {
statusLabel,
statusVariant,
statusIcon,
typeLabel,
typeColor,
halfDayLabel,
formatDate,
formatRange,
formatDays,
}
}