feat : refacto de la partie calendrier + ajout de validation sur les formulaires + ajout des jours fériés
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 pb-10">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<h1 class="text-4xl font-bold text-primary-500">Calendrier des absences</h1>
|
||||
<div class="flex items-center gap-3">
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex flex-wrap items-center gap-4 rounded-md border border-neutral-300 px-3 py-2">
|
||||
<div v-for="site in sites" :key="site.id" class="flex items-center gap-2">
|
||||
<div :style="{ backgroundColor: site.color }" class="h-4 w-4 rounded"></div>
|
||||
@@ -18,7 +20,7 @@
|
||||
</div>
|
||||
<select
|
||||
v-model="selectedMonth"
|
||||
class="rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
|
||||
class="h-10 rounded-md border border-neutral-300 bg-white px-3 text-md text-neutral-900"
|
||||
>
|
||||
<option v-for="month in months" :key="month.value" :value="month.value">
|
||||
{{ month.label }}
|
||||
@@ -26,22 +28,24 @@
|
||||
</select>
|
||||
<select
|
||||
v-model="selectedYear"
|
||||
class="rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
|
||||
class="h-10 rounded-md border border-neutral-300 bg-white px-3 text-md text-neutral-900"
|
||||
>
|
||||
<option v-for="year in years" :key="year" :value="year">
|
||||
{{ year }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||
class="h-10 rounded-lg bg-primary-500 px-4 text-md font-semibold text-white hover:bg-secondary-500"
|
||||
@click="openCreateFromToday"
|
||||
>
|
||||
Ajouter une absence
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||
class="h-10 rounded-lg bg-primary-500 px-4 text-md font-semibold text-white hover:bg-secondary-500"
|
||||
@click="openPrint"
|
||||
>
|
||||
Imprimer
|
||||
@@ -49,196 +53,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-[80vh] overflow-auto rounded-lg border border-neutral-200 bg-white">
|
||||
<div class="min-w-[900px]">
|
||||
<div class="grid" :style="gridStyle">
|
||||
<div
|
||||
class="sticky left-0 z-20 border-b border-neutral-200 bg-tertiary-500 px-4 py-3 text-md font-semibold text-neutral-700">
|
||||
Employés
|
||||
</div>
|
||||
<div
|
||||
v-for="day in daysInMonth"
|
||||
:key="day.date"
|
||||
class="border-b border-neutral-200 bg-tertiary-500 px-2 py-3 text-center text-xs font-semibold text-neutral-700"
|
||||
>
|
||||
<div>{{ day.label }}</div>
|
||||
<div class="text-[10px] text-neutral-500">{{ day.weekday }}</div>
|
||||
</div>
|
||||
<CalendarGrid
|
||||
:days-in-month="daysInMonth"
|
||||
:visible-employees="visibleEmployees"
|
||||
:grid-style="gridStyle"
|
||||
:get-cell-style="getCellStyle"
|
||||
:get-cell-code="getCellCode"
|
||||
:format-employee-name="formatEmployeeName"
|
||||
:is-holiday-date="isHolidayDate"
|
||||
@cell-click="openCreate"
|
||||
/>
|
||||
|
||||
<template v-for="employee in visibleEmployees" :key="employee.id">
|
||||
<div
|
||||
class="sticky left-0 z-10 border-b border-neutral-100 px-4 py-3 text-md font-semibold text-black"
|
||||
:style="{ backgroundColor: employee.site?.color ?? '#304998' }"
|
||||
>
|
||||
{{ formatEmployeeName(employee) }}
|
||||
</div>
|
||||
<div
|
||||
v-for="day in daysInMonth"
|
||||
:key="employee.id + '-' + day.date"
|
||||
class="border-b border-neutral-100 px-2 py-2 text-center text-xs text-neutral-800"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-8 w-full items-center justify-center rounded-md border border-neutral-200 text-[11px] font-semibold text-neutral-800 hover:border-primary-500/40"
|
||||
:style="getCellStyle(employee.id, day.date)"
|
||||
@click="openCreate(employee, day.date)"
|
||||
>
|
||||
<span v-if="getCellCode(employee.id, day.date)">
|
||||
{{ getCellCode(employee.id, day.date) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AbsenceFormDrawer
|
||||
v-model="isDrawerOpen"
|
||||
:employees="employees"
|
||||
:absence-types="absenceTypes"
|
||||
:form="form"
|
||||
:editing-absence="editingAbsence"
|
||||
:is-submitting="isSubmitting"
|
||||
@submit="handleSubmit"
|
||||
@delete="handleDelete"
|
||||
@cancel="closeDrawer"
|
||||
/>
|
||||
|
||||
|
||||
<AppDrawer v-model="isDrawerOpen" title="Nouvelle absence">
|
||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||
<div>
|
||||
<label class="text-md font-semibold text-neutral-700" for="employee">Employé</label>
|
||||
<select
|
||||
id="employee"
|
||||
v-model="form.employeeId"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
|
||||
>
|
||||
<option value="" disabled>Choisir un employé</option>
|
||||
<option v-for="employee in employees" :key="employee.id" :value="employee.id">
|
||||
{{ employee.firstName }} {{ employee.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-md font-semibold text-neutral-700" for="type">Type d'absence</label>
|
||||
<select
|
||||
id="type"
|
||||
v-model="form.typeId"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
|
||||
>
|
||||
<option value="" disabled>Choisir un type</option>
|
||||
<option v-for="type in absenceTypes" :key="type.id" :value="type.id">
|
||||
{{ type.label }} ({{ type.code }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-md font-semibold text-neutral-700" for="start-date">Date de début</label>
|
||||
<input
|
||||
id="start-date"
|
||||
v-model="form.startDate"
|
||||
type="date"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-md font-semibold text-neutral-700" for="end-date">Date de fin</label>
|
||||
<input
|
||||
id="end-date"
|
||||
v-model="form.endDate"
|
||||
type="date"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-md font-semibold text-neutral-700" for="comment">Commentaire</label>
|
||||
<textarea
|
||||
id="comment"
|
||||
v-model="form.comment"
|
||||
rows="3"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<button
|
||||
v-if="editingAbsence"
|
||||
type="button"
|
||||
class="rounded-lg border border-red-200 px-4 py-2 text-md font-semibold text-red-600 hover:bg-red-50"
|
||||
@click="handleDelete"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
||||
@click="closeDrawer"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AppDrawer>
|
||||
|
||||
<AppDrawer v-model="isPrintOpen" title="Imprimer les absences">
|
||||
<form class="space-y-4" @submit.prevent="handlePrint">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-md font-semibold text-neutral-700" for="print-from">Date de début</label>
|
||||
<input
|
||||
id="print-from"
|
||||
v-model="printForm.from"
|
||||
type="date"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-md font-semibold text-neutral-700" for="print-to">Date de fin</label>
|
||||
<input
|
||||
id="print-to"
|
||||
v-model="printForm.to"
|
||||
type="date"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-md font-semibold text-neutral-700">Sites</p>
|
||||
<div class="flex flex-wrap gap-4 rounded-md border border-neutral-300 px-3 py-2">
|
||||
<div v-for="site in sites" :key="site.id" class="flex items-center gap-2">
|
||||
<div :style="{ backgroundColor: site.color }" class="h-4 w-4 rounded"></div>
|
||||
<label class="text-md" :for="`print-site-${site.id}`">{{ site.name }}</label>
|
||||
<input
|
||||
:id="`print-site-${site.id}`"
|
||||
v-model="printForm.siteIds"
|
||||
:value="site.id"
|
||||
type="checkbox"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
||||
@click="closePrint"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||
>
|
||||
Imprimer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AppDrawer>
|
||||
<AbsencePrintDrawer
|
||||
v-model="isPrintOpen"
|
||||
:sites="sites"
|
||||
:print-form="printForm"
|
||||
@submit="handlePrint"
|
||||
@cancel="closePrint"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -249,17 +93,21 @@ import type {Absence} from '~/services/dto/absence'
|
||||
import {listEmployees} from '~/services/employees'
|
||||
import {listAbsenceTypes} from '~/services/absence-types'
|
||||
import {createAbsence, deleteAbsence, listAbsences, updateAbsence} from '~/services/absences'
|
||||
import {listPublicHolidays} from '~/services/public-holidays'
|
||||
import {getDaysInMonth, normalizeDate, toYmd} from '~/utils/date'
|
||||
import CalendarGrid from '~/components/CalendarGrid.vue'
|
||||
import AbsenceFormDrawer from '~/components/AbsenceFormDrawer.vue'
|
||||
import AbsencePrintDrawer from '~/components/AbsencePrintDrawer.vue'
|
||||
|
||||
const employees = ref<Employee[]>([])
|
||||
const sites = computed(() => {
|
||||
const map = new Map<number, { id: number; name: string; color: string }>()
|
||||
const siteMap = new Map<number, { id: number; name: string; color: string }>()
|
||||
for (const employee of employees.value) {
|
||||
if (employee.site) {
|
||||
map.set(employee.site.id, employee.site)
|
||||
siteMap.set(employee.site.id, employee.site)
|
||||
}
|
||||
}
|
||||
return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name, 'fr'))
|
||||
return Array.from(siteMap.values()).sort((siteA, siteB) => siteA.name.localeCompare(siteB.name, 'fr'))
|
||||
})
|
||||
|
||||
const selectedSiteIds = ref<number[]>([])
|
||||
@@ -272,16 +120,16 @@ watch(sites, (next) => {
|
||||
}, { immediate: true })
|
||||
|
||||
const sortedEmployees = computed(() => {
|
||||
return [...employees.value].sort((a, b) => {
|
||||
const siteA = a.site?.name ?? ''
|
||||
const siteB = b.site?.name ?? ''
|
||||
if (siteA !== siteB) return siteA.localeCompare(siteB, 'fr')
|
||||
const lastA = a.lastName ?? ''
|
||||
const lastB = b.lastName ?? ''
|
||||
if (lastA !== lastB) return lastA.localeCompare(lastB, 'fr')
|
||||
const firstA = a.firstName ?? ''
|
||||
const firstB = b.firstName ?? ''
|
||||
return firstA.localeCompare(firstB, 'fr')
|
||||
return [...employees.value].sort((employeeA, employeeB) => {
|
||||
const siteNameA = employeeA.site?.name ?? ''
|
||||
const siteNameB = employeeB.site?.name ?? ''
|
||||
if (siteNameA !== siteNameB) return siteNameA.localeCompare(siteNameB, 'fr')
|
||||
const lastNameA = employeeA.lastName ?? ''
|
||||
const lastNameB = employeeB.lastName ?? ''
|
||||
if (lastNameA !== lastNameB) return lastNameA.localeCompare(lastNameB, 'fr')
|
||||
const firstNameA = employeeA.firstName ?? ''
|
||||
const firstNameB = employeeB.firstName ?? ''
|
||||
return firstNameA.localeCompare(firstNameB, 'fr')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -293,6 +141,7 @@ const visibleEmployees = computed(() => {
|
||||
})
|
||||
const absenceTypes = ref<AbsenceType[]>([])
|
||||
const absences = ref<Absence[]>([])
|
||||
const publicHolidays = ref<Record<string, string>>({})
|
||||
|
||||
const isDrawerOpen = ref(false)
|
||||
const isSubmitting = ref(false)
|
||||
@@ -318,10 +167,12 @@ const months = [
|
||||
{value: 11, label: 'Décembre'}
|
||||
]
|
||||
|
||||
const years = Array.from({length: 5}, (_, i) => now.getFullYear() - 2 + i)
|
||||
const years = Array.from({length: 5}, (unusedValue, index) => now.getFullYear() - 2 + index)
|
||||
|
||||
|
||||
const daysInMonth = computed(() => getDaysInMonth(selectedYear.value, selectedMonth.value))
|
||||
const monthStartDate = computed(() => new Date(selectedYear.value, selectedMonth.value, 1))
|
||||
const monthEndDate = computed(() => new Date(selectedYear.value, selectedMonth.value + 1, 0))
|
||||
|
||||
const gridStyle = computed(() => ({
|
||||
gridTemplateColumns: `220px repeat(${daysInMonth.value.length}, minmax(44px, 1fr))`
|
||||
@@ -423,33 +274,82 @@ const loadAbsenceTypes = async () => {
|
||||
absenceTypes.value = await listAbsenceTypes()
|
||||
}
|
||||
|
||||
const loadPublicHolidays = async () => {
|
||||
publicHolidays.value = await listPublicHolidays('metropole', selectedYear.value)
|
||||
}
|
||||
|
||||
const loadAbsences = async () => {
|
||||
absences.value = await listAbsences()
|
||||
const monthStart = toYmd(selectedYear.value, selectedMonth.value, 1)
|
||||
const monthEnd = toYmd(selectedYear.value, selectedMonth.value + 1, 0)
|
||||
absences.value = await listAbsences({
|
||||
from: monthStart,
|
||||
to: monthEnd,
|
||||
siteIds: selectedSiteIds.value
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadEmployees(), loadAbsenceTypes(), loadAbsences()])
|
||||
await Promise.all([loadEmployees(), loadAbsenceTypes(), loadPublicHolidays(), loadAbsences()])
|
||||
})
|
||||
|
||||
watch([selectedMonth, selectedYear], async () => {
|
||||
watch([selectedMonth, selectedYear, selectedSiteIds], async () => {
|
||||
await loadAbsences()
|
||||
})
|
||||
|
||||
const getCellAbsence = (employeeId: number, date: string) => {
|
||||
const match = absences.value.find((absence) => {
|
||||
const employee = absence.employee?.id
|
||||
const start = normalizeDate(absence.startDate)
|
||||
const end = normalizeDate(absence.endDate)
|
||||
return Number(employee) === employeeId && date >= start && date <= end
|
||||
})
|
||||
watch(selectedYear, async () => {
|
||||
await loadPublicHolidays()
|
||||
})
|
||||
|
||||
if (!match) return null
|
||||
// Indexation des absences par cellule pour eviter un find() a chaque case.
|
||||
const cellAbsenceMap = computed(() => {
|
||||
const map = new Map<string, { id: number; code: string; color: string; textColor?: string }>()
|
||||
const monthStart = monthStartDate.value
|
||||
const monthEnd = monthEndDate.value
|
||||
|
||||
return {
|
||||
id: match.id,
|
||||
code: match.type?.code ?? '',
|
||||
color: match.type?.color ?? '#222783'
|
||||
for (const absence of absences.value) {
|
||||
const employeeId = absence.employee?.id
|
||||
if (!employeeId) continue
|
||||
const start = parseYmd(normalizeDate(absence.startDate))
|
||||
const end = parseYmd(normalizeDate(absence.endDate))
|
||||
if (!start || !end) continue
|
||||
|
||||
const rangeStart = start < monthStart ? monthStart : start
|
||||
const rangeEnd = end > monthEnd ? monthEnd : end
|
||||
if (rangeEnd < rangeStart) continue
|
||||
|
||||
for (
|
||||
let currentDate = new Date(rangeStart.getTime());
|
||||
currentDate <= rangeEnd;
|
||||
currentDate.setDate(currentDate.getDate() + 1)
|
||||
) {
|
||||
const key = `${employeeId}-${toYmd(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate())}`
|
||||
map.set(key, {
|
||||
id: absence.id,
|
||||
code: absence.type?.code ?? '',
|
||||
color: absence.type?.color ?? '#222783'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
})
|
||||
|
||||
const isHolidayDate = (date: string) => {
|
||||
return Boolean(publicHolidays.value[date])
|
||||
}
|
||||
|
||||
const getCellAbsence = (employeeId: number, date: string) => {
|
||||
if (isHolidayDate(date)) {
|
||||
return {
|
||||
id: 0,
|
||||
code: 'F',
|
||||
color: '#b3e5fc',
|
||||
textColor: '#0f172a'
|
||||
}
|
||||
}
|
||||
const absence = cellAbsenceMap.value.get(`${employeeId}-${date}`)
|
||||
if (absence) return absence
|
||||
return null
|
||||
}
|
||||
|
||||
const getCellStyle = (employeeId: number, date: string) => {
|
||||
@@ -458,7 +358,7 @@ const getCellStyle = (employeeId: number, date: string) => {
|
||||
|
||||
return {
|
||||
backgroundColor: absence.color,
|
||||
color: '#fff'
|
||||
color: absence.textColor ?? '#fff'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,6 +367,11 @@ const getCellCode = (employeeId: number, date: string) => {
|
||||
}
|
||||
|
||||
const openCreate = (employee: Employee, date: string) => {
|
||||
if (isHolidayDate(date)) {
|
||||
window.alert("Impossible de creer une absence un jour ferie.")
|
||||
return
|
||||
}
|
||||
|
||||
const existing = absences.value.find((absence) => {
|
||||
const start = normalizeDate(absence.startDate)
|
||||
const end = normalizeDate(absence.endDate)
|
||||
@@ -498,12 +403,33 @@ const openCreateFromToday = () => {
|
||||
form.typeId = ''
|
||||
const now = new Date()
|
||||
const today = toYmd(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
if (isHolidayDate(today)) {
|
||||
window.alert("Impossible de creer une absence un jour ferie.")
|
||||
return
|
||||
}
|
||||
form.startDate = today
|
||||
form.endDate = today
|
||||
form.comment = ''
|
||||
isDrawerOpen.value = true
|
||||
}
|
||||
|
||||
const hasHolidayInRange = (startDate: string, endDate: string) => {
|
||||
const start = parseYmd(startDate)
|
||||
const end = parseYmd(endDate)
|
||||
if (!start || !end) return false
|
||||
for (
|
||||
let currentDate = new Date(start.getTime());
|
||||
currentDate <= end;
|
||||
currentDate.setDate(currentDate.getDate() + 1)
|
||||
) {
|
||||
const key = toYmd(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate())
|
||||
if (isHolidayDate(key)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isSubmitting.value) return
|
||||
|
||||
@@ -511,6 +437,10 @@ const handleSubmit = async () => {
|
||||
try {
|
||||
const start = normalizeDate(form.startDate)
|
||||
const end = normalizeDate(form.endDate)
|
||||
if (hasHolidayInRange(start, end)) {
|
||||
window.alert("Impossible de creer une absence sur un jour ferie.")
|
||||
return
|
||||
}
|
||||
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
|
||||
@@ -519,8 +449,15 @@ const handleSubmit = async () => {
|
||||
return start <= aEnd && end >= aStart
|
||||
})
|
||||
|
||||
for (const overlap of overlaps) {
|
||||
await deleteAbsence(overlap.id)
|
||||
if (overlaps.length > 0) {
|
||||
// Securise le chevauchement: on demande confirmation avant suppression.
|
||||
const confirmReplace = window.confirm(
|
||||
"Cette absence chevauche une autre. Voulez-vous la remplacer ?"
|
||||
)
|
||||
if (!confirmReplace) return
|
||||
for (const overlap of overlaps) {
|
||||
await deleteAbsence(overlap.id)
|
||||
}
|
||||
}
|
||||
|
||||
if (editingAbsence.value) {
|
||||
@@ -552,8 +489,8 @@ const handleSubmit = async () => {
|
||||
const handleDelete = async () => {
|
||||
if (!editingAbsence.value) return
|
||||
|
||||
const ok = window.confirm('Supprimer cette absence ?')
|
||||
if (!ok) return
|
||||
const confirmDelete = window.confirm('Supprimer cette absence ?')
|
||||
if (!confirmDelete) return
|
||||
|
||||
await deleteAbsence(editingAbsence.value.id)
|
||||
closeDrawer()
|
||||
|
||||
Reference in New Issue
Block a user