732 lines
25 KiB
Vue
732 lines
25 KiB
Vue
<template>
|
|
<div class="h-full flex flex-col overflow-hidden">
|
|
<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>
|
|
<div class="flex flex-col gap-3 py-6">
|
|
<div class="flex items-center justify-between gap-4">
|
|
<div class="flex items-center gap-4">
|
|
<SiteFilterSelector v-model="selectedSiteIds" :sites="sites"/>
|
|
</div>
|
|
<div class="flex gap-4">
|
|
<button
|
|
type="button"
|
|
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="h-10 rounded-lg bg-primary-500 px-4 text-md font-semibold text-white hover:bg-secondary-500"
|
|
@click="openPrint"
|
|
>
|
|
Imprimer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<div class="w-80">
|
|
<EmployeeNameFilterInput v-model="employeeFilter"/>
|
|
</div>
|
|
<PeriodStepperPicker
|
|
width-class="w-[260px]"
|
|
:label="selectedMonthLabel"
|
|
picker-type="month"
|
|
:picker-value="monthPickerValue"
|
|
prev-aria-label="Mois précédent"
|
|
next-aria-label="Mois suivant"
|
|
@prev="shiftMonth(-1)"
|
|
@next="shiftMonth(1)"
|
|
@pick="onMonthPickerValue"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-6 py-2">
|
|
<p class="font-bold">Légende :</p>
|
|
<div v-for="type in absenceTypes" :key="type.id" class="flex items-center gap-2">
|
|
<div :style="{ backgroundColor: type.color }" class="h-4 w-4 rounded"></div>
|
|
<p>{{ type.label }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex-1 min-h-0">
|
|
<CalendarGrid
|
|
:days-in-month="daysInMonth"
|
|
:visible-employees="visibleEmployees"
|
|
:grid-style="gridStyle"
|
|
:get-cell-style="getCellStyle"
|
|
:get-cell-info="getCellInfo"
|
|
:format-employee-name="formatEmployeeName"
|
|
:is-holiday-date="isHolidayDate"
|
|
@cell-click="openCreate"
|
|
@reorder="handleReorder"
|
|
/>
|
|
</div>
|
|
|
|
<AbsenceFormDrawer
|
|
v-model="isDrawerOpen"
|
|
:employees="employees"
|
|
:absence-types="absenceTypes"
|
|
:form="form"
|
|
:editing-absence="editingAbsence"
|
|
:is-submitting="isSubmitting"
|
|
@submit="handleSubmit"
|
|
@delete="handleDelete"
|
|
@cancel="closeDrawer"
|
|
/>
|
|
|
|
<AbsencePrintDrawer
|
|
v-model="isPrintOpen"
|
|
:sites="sites"
|
|
:contract-natures="contractNatureOptions"
|
|
:work-contracts="workContractOptions"
|
|
:print-form="printForm"
|
|
@submit="handlePrint"
|
|
@cancel="closePrint"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type {Employee} from '~/services/dto/employee'
|
|
import type {AbsenceType} from '~/services/dto/absence-type'
|
|
import type {Absence} from '~/services/dto/absence'
|
|
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 {listPublicHolidays} from '~/services/public-holidays'
|
|
import {getDaysInMonth, normalizeDate, parseYmd, toYmd} from '~/utils/date'
|
|
import {compareEmployeesInSite, sortEmployeesBySiteAndOrder} from '~/utils/employee'
|
|
import CalendarGrid from '~/components/CalendarGrid.vue'
|
|
import AbsenceFormDrawer from '~/components/AbsenceFormDrawer.vue'
|
|
import AbsencePrintDrawer from '~/components/AbsencePrintDrawer.vue'
|
|
import EmployeeNameFilterInput from '~/components/EmployeeNameFilterInput.vue'
|
|
import PeriodStepperPicker from '~/components/PeriodStepperPicker.vue'
|
|
import SiteFilterSelector from '~/components/SiteFilterSelector.vue'
|
|
|
|
useHead({
|
|
title: 'Calendrier'
|
|
})
|
|
|
|
// Données principales affichées dans la grille.
|
|
const employees = ref<Employee[]>([])
|
|
const sites = computed(() => {
|
|
const siteMap = new Map<number, { id: number; name: string; color: string }>()
|
|
for (const employee of employees.value) {
|
|
if (employee.site) {
|
|
siteMap.set(employee.site.id, employee.site)
|
|
}
|
|
}
|
|
return Array.from(siteMap.values()).sort((siteA, siteB) => {
|
|
const orderA = siteA.displayOrder ?? 0
|
|
const orderB = siteB.displayOrder ?? 0
|
|
if (orderA !== orderB) return orderA - orderB
|
|
return siteA.name.localeCompare(siteB.name, 'fr')
|
|
})
|
|
})
|
|
|
|
// Filtres de sites (par défaut: tous sélectionnés à l'init).
|
|
const selectedSiteIds = ref<number[]>([])
|
|
const sitesInitialized = ref(false)
|
|
|
|
watch(sites, (next) => {
|
|
if (sitesInitialized.value || next.length === 0) return
|
|
selectedSiteIds.value = next.map((site) => site.id)
|
|
sitesInitialized.value = true
|
|
}, {immediate: true})
|
|
|
|
// Tri stable: site -> nom -> prénom.
|
|
const sortedEmployees = computed(() => {
|
|
return sortEmployeesBySiteAndOrder(employees.value)
|
|
})
|
|
|
|
// Employés visibles selon le filtre de sites.
|
|
const employeeFilter = ref('')
|
|
|
|
const visibleEmployees = computed(() => {
|
|
if (selectedSiteIds.value.length === 0) return []
|
|
const filter = employeeFilter.value.trim().toLowerCase()
|
|
return sortedEmployees.value.filter((employee) => {
|
|
const siteOk = employee.site?.id && selectedSiteIds.value.includes(employee.site.id)
|
|
if (!siteOk) return false
|
|
if (!filter) return true
|
|
const first = employee.firstName?.toLowerCase() ?? ''
|
|
const last = employee.lastName?.toLowerCase() ?? ''
|
|
return first.includes(filter) || last.includes(filter)
|
|
})
|
|
})
|
|
// Données de référence et absences du mois affiché.
|
|
const absenceTypes = ref<AbsenceType[]>([])
|
|
const absences = ref<Absence[]>([])
|
|
const publicHolidays = ref<Record<string, string>>({})
|
|
|
|
// États UI.
|
|
const isDrawerOpen = ref(false)
|
|
const isSubmitting = ref(false)
|
|
const editingAbsence = ref<Absence | null>(null)
|
|
const isPrintOpen = ref(false)
|
|
|
|
// Sélecteurs de période.
|
|
const now = new Date()
|
|
const selectedMonth = ref(now.getMonth())
|
|
const selectedYear = ref(now.getFullYear())
|
|
|
|
const months = [
|
|
{value: 0, label: 'Janvier'},
|
|
{value: 1, label: 'Février'},
|
|
{value: 2, label: 'Mars'},
|
|
{value: 3, label: 'Avril'},
|
|
{value: 4, label: 'Mai'},
|
|
{value: 5, label: 'Juin'},
|
|
{value: 6, label: 'Juillet'},
|
|
{value: 7, label: 'Août'},
|
|
{value: 8, label: 'Septembre'},
|
|
{value: 9, label: 'Octobre'},
|
|
{value: 10, label: 'Novembre'},
|
|
{value: 11, label: 'Décembre'}
|
|
]
|
|
|
|
const selectedMonthLabel = computed(() => `${months[selectedMonth.value]?.label ?? ''}`)
|
|
const monthPickerValue = computed(() => `${selectedYear.value}-${String(selectedMonth.value + 1).padStart(2, '0')}`)
|
|
|
|
// Infos de calendrier calculées.
|
|
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))
|
|
|
|
// Largeur fixe de la colonne employés + une colonne par jour.
|
|
const gridStyle = computed(() => ({
|
|
gridTemplateColumns: `160px repeat(${daysInMonth.value.length}, minmax(44px, 1fr))`
|
|
}))
|
|
|
|
// Formulaire d'absence (AM/PM par défaut = journée complète).
|
|
const form = reactive({
|
|
employeeId: '' as number | '',
|
|
typeId: '' as number | '',
|
|
startDate: '',
|
|
startHalf: 'AM' as HalfDay,
|
|
endDate: '',
|
|
endHalf: 'PM' as HalfDay,
|
|
comment: ''
|
|
})
|
|
|
|
// Formulaire d'impression (intervalle + sites).
|
|
const printForm = reactive({
|
|
from: '',
|
|
to: '',
|
|
siteIds: [] as number[],
|
|
contractNatures: [] as Array<'CDI' | 'CDD' | 'INTERIM'>,
|
|
workContractIds: [] as number[]
|
|
})
|
|
|
|
const contractNatureOptions = [
|
|
{ value: 'CDI' as const, label: 'CDI' },
|
|
{ value: 'CDD' as const, label: 'CDD' },
|
|
{ value: 'INTERIM' as const, label: 'Intérim' }
|
|
]
|
|
|
|
const workContractOptions = computed(() => {
|
|
const byId = new Map<number, { id: number; name: string }>()
|
|
for (const employee of employees.value) {
|
|
const contract = employee.contract
|
|
if (!contract?.id) continue
|
|
byId.set(contract.id, { id: contract.id, name: contract.name })
|
|
}
|
|
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name, 'fr'))
|
|
})
|
|
|
|
// Remet le formulaire à zéro.
|
|
const resetForm = () => {
|
|
form.employeeId = ''
|
|
form.typeId = ''
|
|
form.startDate = ''
|
|
form.startHalf = 'AM'
|
|
form.endDate = ''
|
|
form.endHalf = 'PM'
|
|
form.comment = ''
|
|
}
|
|
|
|
// Ferme le drawer et nettoie l'état.
|
|
const closeDrawer = () => {
|
|
isDrawerOpen.value = false
|
|
editingAbsence.value = null
|
|
resetForm()
|
|
}
|
|
|
|
// Ouvre l'impression avec la période du mois courant.
|
|
const openPrint = () => {
|
|
const monthStart = toYmd(selectedYear.value, selectedMonth.value, 1)
|
|
const monthEnd = toYmd(selectedYear.value, selectedMonth.value + 1, 0)
|
|
printForm.from = monthStart
|
|
printForm.to = monthEnd
|
|
printForm.siteIds = [...selectedSiteIds.value]
|
|
printForm.contractNatures = contractNatureOptions.map((item) => item.value)
|
|
printForm.workContractIds = workContractOptions.value.map((item) => item.id)
|
|
isPrintOpen.value = true
|
|
}
|
|
|
|
const closePrint = () => {
|
|
isPrintOpen.value = false
|
|
}
|
|
|
|
// Détermine si la journée est une demi-journée (AM/PM) ou complète.
|
|
const getHalfForDate = (
|
|
startDate: string,
|
|
endDate: string,
|
|
startHalf: HalfDay,
|
|
endHalf: HalfDay,
|
|
date: string
|
|
) => {
|
|
if (startDate === endDate) {
|
|
return startHalf === endHalf ? startHalf : null
|
|
}
|
|
if (date === startDate && startHalf === 'PM') return 'PM'
|
|
if (date === endDate && endHalf === 'AM') return 'AM'
|
|
return null
|
|
}
|
|
|
|
// Renvoie les segments occupés pour une date donnée (AM/PM).
|
|
const getSegmentsForDate = (
|
|
startDate: string,
|
|
endDate: string,
|
|
startHalf: HalfDay,
|
|
endHalf: HalfDay,
|
|
date: string
|
|
) => {
|
|
const half = getHalfForDate(startDate, endDate, startHalf, endHalf, date)
|
|
if (!half) return HALF_DAYS.map((item) => item.value) as HalfDay[]
|
|
return [half] as HalfDay[]
|
|
}
|
|
|
|
// Ajoute des mois tout en gardant un jour valide.
|
|
const addMonths = (date: Date, months: number) => {
|
|
const next = new Date(date.getFullYear(), date.getMonth() + months, date.getDate())
|
|
if (next.getMonth() !== (date.getMonth() + months) % 12) {
|
|
next.setDate(0)
|
|
}
|
|
return next
|
|
}
|
|
|
|
const shiftMonth = (delta: number) => {
|
|
const next = new Date(selectedYear.value, selectedMonth.value + delta, 1)
|
|
selectedYear.value = next.getFullYear()
|
|
selectedMonth.value = next.getMonth()
|
|
}
|
|
|
|
const onMonthPickerValue = (value: string) => {
|
|
if (!value) return
|
|
const [yearStr, monthStr] = value.split('-')
|
|
const year = Number(yearStr)
|
|
const month = Number(monthStr)
|
|
if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12) return
|
|
selectedYear.value = year
|
|
selectedMonth.value = month - 1
|
|
}
|
|
|
|
// Limite l'intervalle d'impression à 2 mois max.
|
|
const enforcePrintRange = () => {
|
|
if (!printForm.from) return
|
|
const start = parseYmd(printForm.from)
|
|
if (!start) return
|
|
const maxEnd = addMonths(start, 2)
|
|
maxEnd.setDate(maxEnd.getDate() - 1)
|
|
const maxEndYmd = toYmd(maxEnd.getFullYear(), maxEnd.getMonth(), maxEnd.getDate())
|
|
|
|
if (!printForm.to) {
|
|
printForm.to = maxEndYmd
|
|
return
|
|
}
|
|
|
|
const end = parseYmd(printForm.to)
|
|
if (!end) {
|
|
printForm.to = maxEndYmd
|
|
return
|
|
}
|
|
|
|
if (end < start) {
|
|
printForm.to = printForm.from
|
|
return
|
|
}
|
|
|
|
if (end > maxEnd) {
|
|
printForm.to = maxEndYmd
|
|
}
|
|
}
|
|
|
|
watch(() => printForm.from, enforcePrintRange)
|
|
watch(() => printForm.to, enforcePrintRange)
|
|
|
|
// Chargements API.
|
|
const loadEmployees = async () => {
|
|
employees.value = await listEmployees()
|
|
}
|
|
|
|
const loadAbsenceTypes = async () => {
|
|
absenceTypes.value = await listAbsenceTypes()
|
|
}
|
|
|
|
const loadPublicHolidays = async () => {
|
|
publicHolidays.value = await listPublicHolidays('metropole', selectedYear.value)
|
|
}
|
|
|
|
const loadAbsences = async () => {
|
|
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(), loadPublicHolidays(), loadAbsences()])
|
|
})
|
|
|
|
watch([selectedMonth, selectedYear, selectedSiteIds], async () => {
|
|
await loadAbsences()
|
|
})
|
|
|
|
watch(selectedYear, async () => {
|
|
await loadPublicHolidays()
|
|
})
|
|
|
|
// 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; halfLabel?: HalfDay; textColor?: string }>()
|
|
const monthStart = monthStartDate.value
|
|
const monthEnd = monthEndDate.value
|
|
|
|
for (const absence of absences.value) {
|
|
const employeeId = absence.employee?.id
|
|
if (!employeeId) continue
|
|
const startDate = normalizeDate(absence.startDate)
|
|
const endDate = normalizeDate(absence.endDate)
|
|
const start = parseYmd(startDate)
|
|
const end = parseYmd(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 dateKey = toYmd(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate())
|
|
const key = `${employeeId}-${dateKey}`
|
|
const halfLabel = getHalfForDate(
|
|
startDate,
|
|
endDate,
|
|
absence.startHalf ?? 'AM',
|
|
absence.endHalf ?? 'PM',
|
|
dateKey
|
|
) ?? undefined
|
|
map.set(key, {
|
|
id: absence.id,
|
|
code: absence.type?.code ?? '',
|
|
color: absence.type?.color ?? '#222783',
|
|
halfLabel
|
|
})
|
|
}
|
|
}
|
|
|
|
return map
|
|
})
|
|
|
|
// Jours fériés (interdit pour la création).
|
|
const isHolidayDate = (date: string) => {
|
|
return Boolean(publicHolidays.value[date])
|
|
}
|
|
|
|
// Renvoie l'absence effective pour une cellule (ou un "Férié").
|
|
const getCellAbsence = (employeeId: number, date: string) => {
|
|
if (isHolidayDate(date)) {
|
|
return {
|
|
id: 0,
|
|
code: 'Férié',
|
|
color: '#b3e5fc',
|
|
textColor: '#0f172a'
|
|
}
|
|
}
|
|
const absence = cellAbsenceMap.value.get(`${employeeId}-${date}`)
|
|
if (absence) return absence
|
|
return null
|
|
}
|
|
|
|
// Style de cellule (plein ou demi-journée).
|
|
const getCellStyle = (employeeId: number, date: string) => {
|
|
const absence = getCellAbsence(employeeId, date)
|
|
if (!absence) return undefined
|
|
|
|
if (absence.halfLabel) {
|
|
const color = absence.color
|
|
const textColor = absence.textColor ?? '#FFF'
|
|
const backgroundImage = absence.halfLabel === 'AM'
|
|
? `linear-gradient(180deg, ${color} 0 50%, transparent 50% 100%)`
|
|
: `linear-gradient(180deg, transparent 0 50%, ${color} 50% 100%)`
|
|
return {
|
|
backgroundImage,
|
|
backgroundColor: 'transparent',
|
|
color: textColor
|
|
}
|
|
}
|
|
|
|
return {
|
|
backgroundColor: absence.color,
|
|
color: absence.textColor ?? '#fff'
|
|
}
|
|
}
|
|
|
|
const getCellInfo = (employeeId: number, date: string) => {
|
|
return getCellAbsence(employeeId, date)
|
|
}
|
|
|
|
// Ouverture du drawer depuis une cellule.
|
|
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)
|
|
return absence.employee?.id === employee.id && date >= start && date <= end
|
|
})
|
|
|
|
if (existing) {
|
|
editingAbsence.value = existing
|
|
form.employeeId = existing.employee.id
|
|
form.typeId = existing.type.id
|
|
form.startDate = normalizeDate(existing.startDate)
|
|
form.endDate = normalizeDate(existing.endDate)
|
|
form.startHalf = existing.startHalf ?? 'AM'
|
|
form.endHalf = existing.endHalf ?? 'PM'
|
|
form.comment = existing.comment ?? ''
|
|
} else {
|
|
editingAbsence.value = null
|
|
form.employeeId = employee.id
|
|
form.startDate = date
|
|
form.endDate = date
|
|
form.startHalf = 'AM'
|
|
form.endHalf = 'PM'
|
|
form.typeId = ''
|
|
form.comment = ''
|
|
}
|
|
|
|
isDrawerOpen.value = true
|
|
}
|
|
|
|
// Ouverture du drawer depuis le bouton "Ajouter".
|
|
const openCreateFromToday = () => {
|
|
editingAbsence.value = null
|
|
form.employeeId = ''
|
|
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.startHalf = 'AM'
|
|
form.endHalf = 'PM'
|
|
form.comment = ''
|
|
isDrawerOpen.value = true
|
|
}
|
|
|
|
// Vérifie la présence d'un férié dans l'intervalle.
|
|
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
|
|
}
|
|
|
|
// Soumission du formulaire: validations + chevauchement + save.
|
|
const handleSubmit = async () => {
|
|
if (isSubmitting.value) return
|
|
|
|
isSubmitting.value = true
|
|
try {
|
|
const start = normalizeDate(form.startDate)
|
|
const end = normalizeDate(form.endDate)
|
|
if (start > end) {
|
|
window.alert("La date de fin ne peut pas etre avant la date de debut.")
|
|
return
|
|
}
|
|
if (start === end && form.startHalf === 'PM' && form.endHalf === 'AM') {
|
|
window.alert("La demi-journee de fin ne peut pas etre avant la demi-journee de debut.")
|
|
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
|
|
const aStart = normalizeDate(absence.startDate)
|
|
const aEnd = normalizeDate(absence.endDate)
|
|
if (start > aEnd || end < aStart) return false
|
|
|
|
const overlapStart = start > aStart ? start : aStart
|
|
const overlapEnd = end < aEnd ? end : aEnd
|
|
const overlapStartDate = parseYmd(overlapStart)
|
|
const overlapEndDate = parseYmd(overlapEnd)
|
|
if (!overlapStartDate || !overlapEndDate) return false
|
|
|
|
for (
|
|
let currentDate = new Date(overlapStartDate.getTime());
|
|
currentDate <= overlapEndDate;
|
|
currentDate.setDate(currentDate.getDate() + 1)
|
|
) {
|
|
const dateKey = toYmd(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate())
|
|
const existingSegments = getSegmentsForDate(
|
|
aStart,
|
|
aEnd,
|
|
absence.startHalf ?? 'AM',
|
|
absence.endHalf ?? 'PM',
|
|
dateKey
|
|
)
|
|
const newSegments = getSegmentsForDate(
|
|
start,
|
|
end,
|
|
form.startHalf,
|
|
form.endHalf,
|
|
dateKey
|
|
)
|
|
if (existingSegments.some((segment) => newSegments.includes(segment))) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
})
|
|
|
|
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) {
|
|
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
|
|
})
|
|
}
|
|
|
|
closeDrawer()
|
|
await loadAbsences()
|
|
} finally {
|
|
isSubmitting.value = false
|
|
}
|
|
}
|
|
|
|
// Suppression de l'absence en cours d'édition.
|
|
const handleDelete = async () => {
|
|
if (!editingAbsence.value) return
|
|
|
|
const confirmDelete = window.confirm('Supprimer cette absence ?')
|
|
if (!confirmDelete) return
|
|
|
|
await deleteAbsence(editingAbsence.value.id)
|
|
closeDrawer()
|
|
await loadAbsences()
|
|
}
|
|
|
|
// Affiche "Prénom N.".
|
|
const formatEmployeeName = (employee: Employee) => {
|
|
const initial = employee.lastName ? `${employee.lastName[0].toUpperCase()}.` : ''
|
|
return `${employee.firstName} ${initial}`.trim()
|
|
}
|
|
|
|
// Impression PDF de l'intervalle sélectionné.
|
|
const {printPdf} = usePdfPrinter()
|
|
const handlePrint = async () => {
|
|
const params = new URLSearchParams()
|
|
params.set('from', printForm.from)
|
|
params.set('to', printForm.to)
|
|
if (printForm.siteIds.length > 0) {
|
|
params.set('sites', printForm.siteIds.join(','))
|
|
}
|
|
if (printForm.contractNatures.length > 0) {
|
|
params.set('contractNatures', printForm.contractNatures.join(','))
|
|
}
|
|
if (printForm.workContractIds.length > 0) {
|
|
params.set('workContracts', printForm.workContractIds.join(','))
|
|
}
|
|
await printPdf(`/absences/print?${params.toString()}`)
|
|
isPrintOpen.value = false
|
|
}
|
|
|
|
const handleReorder = async (payload: { dragId: number; dropId: number }) => {
|
|
const dragEmployee = employees.value.find((employee) => employee.id === payload.dragId)
|
|
const dropEmployee = employees.value.find((employee) => employee.id === payload.dropId)
|
|
if (!dragEmployee || !dropEmployee) return
|
|
const dragSiteId = dragEmployee.site?.id
|
|
const dropSiteId = dropEmployee.site?.id
|
|
if (!dragSiteId || !dropSiteId || dragSiteId !== dropSiteId) return
|
|
|
|
const siteEmployees = [...employees.value]
|
|
.filter((employee) => employee.site?.id === dragSiteId)
|
|
.sort(compareEmployeesInSite)
|
|
|
|
const fromIndex = siteEmployees.findIndex((employee) => employee.id === dragEmployee.id)
|
|
const toIndex = siteEmployees.findIndex((employee) => employee.id === dropEmployee.id)
|
|
if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) return
|
|
|
|
const [moved] = siteEmployees.splice(fromIndex, 1)
|
|
siteEmployees.splice(toIndex, 0, moved)
|
|
|
|
const updates: Array<{ id: number; displayOrder: number }> = []
|
|
siteEmployees.forEach((employee, index) => {
|
|
const nextOrder = index + 1
|
|
if ((employee.displayOrder ?? 0) !== nextOrder) {
|
|
updates.push({id: employee.id, displayOrder: nextOrder})
|
|
}
|
|
employee.displayOrder = nextOrder
|
|
})
|
|
|
|
if (updates.length === 0) return
|
|
await Promise.all(updates.map((update) => updateEmployeeOrder(update.id, update.displayOrder)))
|
|
}
|
|
</script>
|