import { computed, onMounted, ref, watch } from 'vue' import type { Employee } from '~/services/dto/employee' import type { Site } from '~/services/dto/site' import type { WorkHour, WorkHourDayContext, WeeklyWorkHourSummary } from '~/services/dto/work-hour' import type { AbsenceType } from '~/services/dto/absence-type' import type { Absence } from '~/services/dto/absence' import type { HalfDay } from '~/services/dto/half-day' import type { DriverHourRow } from '~/services/dto/work-hour' import { listScopedEmployees } from '~/services/employees' import { listAbsenceTypes } from '~/services/absence-types' import { createAbsence, deleteAbsence, listAbsences, updateAbsence } from '~/services/absences' import { listPublicHolidays } from '~/services/public-holidays' import { bulkUpdateWorkHourSiteValidation, bulkUpdateWorkHourValidation, bulkUpsertWorkHours, getWorkHourDayContext, getWeeklyWorkHourSummary, listWorkHoursByDate, updateWorkHourSiteValidation, updateWorkHourValidation } from '~/services/work-hours' import { formatDateLongFr, formatWeekRangeFr, getIsoWeekNumber, getOffsetFromTodayYmd, getWeekStartDate, getTodayYmd, parseYmd, shiftYmd } from '~/utils/date' import { sortEmployeesBySiteAndOrder } from '~/utils/employee' export const useDriverHoursPage = () => { const auth = useAuthStore() const toast = useToast() const isAdmin = computed(() => auth.user?.roles?.includes('ROLE_ADMIN') ?? false) const isSelfUser = computed(() => auth.user?.roles?.includes('ROLE_SELF') ?? false) const isSiteManager = computed(() => !isAdmin.value && !isSelfUser.value) const viewMode = ref<'day' | 'week'>('day') const selectedDate = ref(getTodayYmd()) const employees = ref([]) const employeeFilter = ref('') const selectedSiteIds = ref([]) const sitesInitialized = ref(false) const rows = ref>({}) const dayContext = ref(null) const weeklySummary = ref(null) const absenceTypes = ref([]) const absences = ref([]) const publicHolidaysByYear = ref>>({}) const isAbsenceDrawerOpen = ref(false) const isAbsenceSubmitting = ref(false) const editingAbsence = ref(null) const absenceForm = ref({ employeeId: '' as number | '', typeId: '' as number | '', startDate: '', startHalf: 'AM' as HalfDay, endDate: '', endHalf: 'PM' as HalfDay, comment: '' }) const isLoading = ref(false) const isWeekLoading = ref(false) const isSubmitting = ref(false) const validatingRowIds = ref([]) const siteValidatingRowIds = ref([]) const dayGridCols = computed(() => { const metricCol = '0.4fr' const validationCols = isAdmin.value ? `${metricCol}` : `${metricCol} ${metricCol}` return `1.2fr 0.6fr 0.8fr 0.8fr 0.8fr ${metricCol} ${metricCol} ${metricCol} ${metricCol} ${metricCol} ${validationCols}` }) const weekGridCols = '1.6fr repeat(7, 0.6fr) repeat(7, 0.6fr) repeat(4, 0.4fr)' const sites = computed(() => { const siteMap = new Map() 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') }) }) const visibleEmployees = computed(() => { if (selectedSiteIds.value.length === 0) return [] const filter = employeeFilter.value.trim().toLowerCase() return employees.value.filter((employee) => { if (employee.isDriver !== true) return false const siteId = employee.site?.id if (!siteId || !selectedSiteIds.value.includes(siteId)) return false if (!filter) return true const firstName = employee.firstName?.toLowerCase() ?? '' const lastName = employee.lastName?.toLowerCase() ?? '' return firstName.includes(filter) || lastName.includes(filter) }) }) const displayedEmployees = computed(() => { return visibleEmployees.value.filter((employee) => hasContractAtSelectedDate(employee.id)) }) const visibleEmployeeIdSet = computed(() => new Set(visibleEmployees.value.map((employee) => employee.id))) const filteredWeeklySummary = computed(() => { if (!weeklySummary.value) return null return { ...weeklySummary.value, rows: weeklySummary.value.rows.filter((row) => visibleEmployeeIdSet.value.has(row.employeeId) && row.hasContractForWeek !== false ) } }) const saveButtonClass = computed(() => { if (isSubmitting.value || employees.value.length === 0) { return 'opacity-50 cursor-not-allowed' } return '' }) const isValidationPending = (employeeId: number) => validatingRowIds.value.includes(employeeId) const isSiteValidationPending = (employeeId: number) => siteValidatingRowIds.value.includes(employeeId) const canToggleValidation = (employeeId: number) => !!rows.value[employeeId]?.workHourId const canToggleSiteValidation = (employeeId: number) => { if (!isSiteManager.value) return false const row = rows.value[employeeId] if (!row?.workHourId) return false if (row.isValid) return false return true } const canCreateEmptyValidationRow = (employeeId: number) => { const row = rows.value[employeeId] if (row?.workHourId) return false if (!hasContractAtSelectedDate(employeeId)) return false const dayRow = dayContextByEmployeeId.value.get(employeeId) return !!dayRow?.absenceLabel } const canCreateValidationRowFromAbsence = (employeeId: number) => canCreateEmptyValidationRow(employeeId) const canCreateSiteValidationRowFromAbsence = (employeeId: number) => canCreateEmptyValidationRow(employeeId) const bulkValidatableEmployeeIds = computed(() => { return visibleEmployees.value .map((employee) => employee.id) .filter((employeeId) => canToggleValidation(employeeId) || canCreateValidationRowFromAbsence(employeeId)) }) const isBulkValidationChecked = computed(() => { const ids = bulkValidatableEmployeeIds.value if (ids.length === 0) return false return ids.every((employeeId) => rows.value[employeeId]?.isValid ?? false) }) const isBulkValidationIndeterminate = computed(() => { const ids = bulkValidatableEmployeeIds.value if (ids.length === 0) return false const checkedCount = ids.filter((employeeId) => rows.value[employeeId]?.isValid ?? false).length return checkedCount > 0 && checkedCount < ids.length }) const bulkSiteValidatableEmployeeIds = computed(() => { if (!isSiteManager.value) return [] return visibleEmployees.value .map((employee) => employee.id) .filter((employeeId) => canToggleSiteValidation(employeeId) || canCreateSiteValidationRowFromAbsence(employeeId)) }) const isBulkSiteValidationChecked = computed(() => { const ids = bulkSiteValidatableEmployeeIds.value if (ids.length === 0) return false return ids.every((employeeId) => rows.value[employeeId]?.isSiteValid ?? false) }) const isBulkSiteValidationIndeterminate = computed(() => { const ids = bulkSiteValidatableEmployeeIds.value if (ids.length === 0) return false const checkedCount = ids.filter((employeeId) => rows.value[employeeId]?.isSiteValid ?? false).length return checkedCount > 0 && checkedCount < ids.length }) const canBulkToggleSiteValidation = computed(() => bulkSiteValidatableEmployeeIds.value.length > 0) const dayContextByEmployeeId = computed(() => { const map = new Map() for (const row of dayContext.value?.rows ?? []) { map.set(row.employeeId, row) } return map }) const shortcutButtonClass = (target: 'yesterday' | 'today' | 'tomorrow') => { const targetDate = target === 'yesterday' ? getOffsetFromTodayYmd(-1) : target === 'tomorrow' ? getOffsetFromTodayYmd(1) : getTodayYmd() if (selectedDate.value === targetDate) { return 'bg-primary-500 text-white' } return 'bg-white text-primary-500 hover:bg-tertiary-500' } const weekShortcutButtonClass = (target: 'previousWeek' | 'thisWeek' | 'nextWeek') => { const selected = parseYmd(selectedDate.value) if (!selected) { return 'bg-white text-primary-500 hover:bg-tertiary-500' } const today = new Date() const targetDate = new Date(today) if (target === 'previousWeek') targetDate.setDate(today.getDate() - 7) if (target === 'nextWeek') targetDate.setDate(today.getDate() + 7) const selectedWeekStart = getWeekStartDate(selected) const targetWeekStart = getWeekStartDate(targetDate) const isActive = selectedWeekStart.getTime() === targetWeekStart.getTime() if (isActive) { return 'bg-primary-500 text-white' } return 'bg-white text-primary-500 hover:bg-tertiary-500' } const getWeekShortcutLabel = (target: 'previousWeek' | 'thisWeek' | 'nextWeek') => { const today = new Date() if (target === 'previousWeek') today.setDate(today.getDate() - 7) if (target === 'nextWeek') today.setDate(today.getDate() + 7) const weekNumber = getIsoWeekNumber(today) return `Sem. S${weekNumber}` } const formattedSelectedDate = computed(() => { const parsed = parseYmd(selectedDate.value) if (!parsed) return selectedDate.value if (viewMode.value === 'week') { return formatWeekRangeFr(parsed) } return formatDateLongFr(parsed) }) const selectedYear = computed(() => { const parsed = parseYmd(selectedDate.value) return parsed ? parsed.getFullYear() : null }) const selectedHolidayLabel = computed(() => { const year = selectedYear.value if (!year) return '' return publicHolidaysByYear.value[year]?.[selectedDate.value] ?? '' }) const isSelectedDateHoliday = computed(() => selectedHolidayLabel.value !== '') const weekDayHeaders = computed(() => { const days = weeklySummary.value?.days ?? [] return days.map((date) => { const parsed = parseYmd(date) if (!parsed) return { date, weekday: '', dayDate: '' } const weekday = new Intl.DateTimeFormat('fr-FR', { weekday: 'short' }).format(parsed) const dayDate = new Intl.DateTimeFormat('fr-FR', { day: '2-digit', month: '2-digit' }).format(parsed) return { date, weekday, dayDate } }) }) const shiftDate = (steps: number) => { const offset = viewMode.value === 'week' ? (steps * 7) : steps const next = shiftYmd(selectedDate.value, offset) if (!next) return selectedDate.value = next } const setToday = () => { selectedDate.value = getTodayYmd() } const setYesterday = () => { setToday(); shiftDate(-1) } const setTomorrow = () => { setToday(); shiftDate(1) } const setThisWeek = () => { selectedDate.value = getTodayYmd() } const setPreviousWeek = () => { const previousWeek = shiftYmd(getTodayYmd(), -7) if (!previousWeek) return selectedDate.value = previousWeek } const setNextWeek = () => { const nextWeek = shiftYmd(getTodayYmd(), 7) if (!nextWeek) return selectedDate.value = nextWeek } const resetAbsenceForm = () => { absenceForm.value = { employeeId: '', typeId: '', startDate: '', startHalf: 'AM', endDate: '', endHalf: 'PM', comment: '' } } const closeAbsenceDrawer = () => { isAbsenceDrawerOpen.value = false editingAbsence.value = null resetAbsenceForm() } const toMinutes = (time: string): number => { if (!time) return 0 const [hours, minutes] = time.split(':').map(Number) if (!Number.isInteger(hours) || !Number.isInteger(minutes)) return 0 return (hours * 60) + minutes } const formatMinutes = (minutes: number) => { const safeMinutes = Math.max(0, minutes) const hours = Math.floor(safeMinutes / 60) const rest = safeMinutes % 60 return `${String(hours).padStart(2, '0')}:${String(rest).padStart(2, '0')}` } const minutesToTimeString = (minutes: number | null | undefined): string => { if (minutes === null || minutes === undefined || minutes === 0) return '' return formatMinutes(minutes) } const emptyRow = (): DriverHourRow => ({ workHourId: null, dayHours: '', nightHours: '', workshopHours: '', hasBreakfast: false, hasLunch: false, hasDinner: false, hasOvernight: false, isSiteValid: false, isValid: false, updatedAt: null }) const isRowLocked = (employeeId: number) => { const row = rows.value[employeeId] if (!row) return false if (row.isValid) return true if (!isAdmin.value && row.isSiteValid) return true return false } const contractLabel = (employee: Employee) => { const contract = employee.contract if (!contract) return '-' return contract.name } const getRowMetrics = (employeeId: number) => { const row = rows.value[employeeId] ?? emptyRow() const credited = dayContextByEmployeeId.value.get(employeeId)?.creditedMinutes ?? 0 const dayMinutes = toMinutes(row.dayHours) + credited const nightMinutes = toMinutes(row.nightHours) const workshopMinutes = toMinutes(row.workshopHours) const totalMinutes = dayMinutes + nightMinutes + workshopMinutes return { dayMinutes, nightMinutes, workshopMinutes, totalMinutes } } const getRowAbsenceLabel = (employeeId: number) => { const dayRow = dayContextByEmployeeId.value.get(employeeId) if (dayRow && dayRow.hasContractAtDate === false) { return 'Contrat non démarré' } if (isSelectedDateHoliday.value) return 'Férié' if (!dayRow?.absenceLabel) return '' if (dayRow.absenceHalf === 'AM' || dayRow.absenceHalf === 'PM') { const halfLabel = dayRow.absenceHalf === 'AM' ? 'Matin' : 'ap.-m.' return `${dayRow.absenceLabel} (${halfLabel})` } return `${dayRow.absenceLabel} (journée)` } const getRowAbsenceStyle = (employeeId: number) => { const dayRow = dayContextByEmployeeId.value.get(employeeId) if (dayRow && dayRow.hasContractAtDate === false) { return { backgroundColor: '#6b7280' } } if (!dayRow?.absenceLabel) return undefined return { backgroundColor: dayRow.absenceColor || '#dc2626' } } const getRowUpdatedAt = (employeeId: number): string => { const raw = rows.value[employeeId]?.updatedAt if (!raw) return '' const date = new Date(raw) if (Number.isNaN(date.getTime())) return '' return date.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) } const hasContractAtSelectedDate = (employeeId: number) => { const dayRow = dayContextByEmployeeId.value.get(employeeId) if (!dayRow) return true return dayRow.hasContractAtDate !== false } const hydrateRows = (workHours: WorkHour[]) => { const byEmployeeId = new Map() for (const workHour of workHours) { byEmployeeId.set(workHour.employee.id, workHour) } const nextRows: Record = {} for (const employee of employees.value) { if (employee.isDriver !== true) continue const workHour = byEmployeeId.get(employee.id) nextRows[employee.id] = { workHourId: workHour?.id ?? null, dayHours: minutesToTimeString(workHour?.dayHoursMinutes), nightHours: minutesToTimeString(workHour?.nightHoursMinutes), workshopHours: minutesToTimeString(workHour?.workshopHoursMinutes), hasBreakfast: workHour?.hasBreakfast ?? false, hasLunch: workHour?.hasLunch ?? false, hasDinner: workHour?.hasDinner ?? false, hasOvernight: workHour?.hasOvernight ?? false, isSiteValid: workHour?.isSiteValid ?? false, isValid: workHour?.isValid ?? false, updatedAt: workHour?.updatedAt ?? null } } rows.value = nextRows } const loadAbsenceTypes = async () => { absenceTypes.value = await listAbsenceTypes() } const loadPublicHolidaysForSelectedYear = async () => { const year = selectedYear.value if (!year) return if (publicHolidaysByYear.value[year]) return const holidays = await listPublicHolidays('metropole', year) publicHolidaysByYear.value = { ...publicHolidaysByYear.value, [year]: holidays } } const loadAbsences = async () => { absences.value = await listAbsences({ from: selectedDate.value, to: selectedDate.value, siteIds: isAdmin.value ? selectedSiteIds.value : undefined }) } const openAbsenceDrawer = (employeeId: number) => { if (!hasContractAtSelectedDate(employeeId)) return if (isSelectedDateHoliday.value) return const existing = absences.value.find((absence) => { if (absence.employee?.id !== employeeId) return false const start = absence.startDate.slice(0, 10) const end = absence.endDate.slice(0, 10) return selectedDate.value >= start && selectedDate.value <= end }) ?? null if (existing) { editingAbsence.value = existing absenceForm.value = { employeeId, typeId: existing.type?.id ?? '', startDate: existing.startDate.slice(0, 10), startHalf: existing.startHalf ?? 'AM', endDate: existing.endDate.slice(0, 10), endHalf: existing.endHalf ?? 'PM', comment: existing.comment ?? '' } } else { editingAbsence.value = null absenceForm.value = { employeeId, typeId: '', startDate: selectedDate.value, startHalf: 'AM', endDate: selectedDate.value, endHalf: 'PM', comment: '' } } isAbsenceDrawerOpen.value = true } const refreshAfterAbsenceChange = async () => { if (isAdmin.value) { await Promise.all([loadWeeklySummary(), loadDayContext(), loadAbsences()]) return } weeklySummary.value = null await Promise.all([loadDayContext(), loadAbsences()]) } const submitAbsence = async () => { const form = absenceForm.value if (isAbsenceSubmitting.value || form.employeeId === '' || form.typeId === '') return isAbsenceSubmitting.value = true try { 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: editingAbsence.value.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: '' }) } closeAbsenceDrawer() await refreshAfterAbsenceChange() } finally { isAbsenceSubmitting.value = false } } const deleteAbsenceFromDrawer = async () => { if (!editingAbsence.value || isAbsenceSubmitting.value) return isAbsenceSubmitting.value = true try { await deleteAbsence(editingAbsence.value.id) closeAbsenceDrawer() await refreshAfterAbsenceChange() } finally { isAbsenceSubmitting.value = false } } const buildEmptyDriverEntry = (employeeId: number) => ({ employeeId, morningFrom: null, morningTo: null, afternoonFrom: null, afternoonTo: null, eveningFrom: null, eveningTo: null, isPresentMorning: false, isPresentAfternoon: false, dayHoursMinutes: null, nightHoursMinutes: null, workshopHoursMinutes: null, hasBreakfast: false, hasLunch: false, hasDinner: false, hasOvernight: false }) const toggleValidation = async ( employeeId: number, checked: boolean, options: { toast?: boolean } = {} ) => { const row = rows.value[employeeId] if (!row?.workHourId && checked) { if (canCreateEmptyValidationRow(employeeId)) { await bulkUpsertWorkHours({ workDate: selectedDate.value, entries: [buildEmptyDriverEntry(employeeId)] }, { toast: false }) await loadWorkHours() } } const updatedRow = rows.value[employeeId] if (!updatedRow?.workHourId) { if (options.toast !== false) { toast.error({ title: 'Validation impossible', message: 'La ligne doit contenir des heures ou une absence.' }) } return } if (isValidationPending(employeeId)) return validatingRowIds.value = [...validatingRowIds.value, employeeId] try { await updateWorkHourValidation(updatedRow.workHourId, checked, { toast: options.toast }) updatedRow.isValid = checked } finally { validatingRowIds.value = validatingRowIds.value.filter((id) => id !== employeeId) } } const toggleSiteValidation = async ( employeeId: number, checked: boolean, options: { toast?: boolean } = {} ) => { const row = rows.value[employeeId] if (!row?.workHourId && checked) { if (canCreateEmptyValidationRow(employeeId)) { await bulkUpsertWorkHours({ workDate: selectedDate.value, entries: [buildEmptyDriverEntry(employeeId)] }, { toast: false }) await loadWorkHours() } } const updatedRow = rows.value[employeeId] if (!updatedRow?.workHourId) { if (options.toast !== false) { toast.error({ title: 'Validation impossible', message: 'La ligne doit contenir des heures ou une absence.' }) } return } if (isSiteValidationPending(employeeId)) return if (!canToggleSiteValidation(employeeId)) return siteValidatingRowIds.value = [...siteValidatingRowIds.value, employeeId] try { await updateWorkHourSiteValidation(updatedRow.workHourId, checked, { toast: options.toast }) updatedRow.isSiteValid = checked } finally { siteValidatingRowIds.value = siteValidatingRowIds.value.filter((id) => id !== employeeId) } } const toggleValidationBulk = async (checked: boolean) => { const employeeIds = bulkValidatableEmployeeIds.value if (employeeIds.length === 0) return const pendingIds = new Set(validatingRowIds.value) const availableEmployeeIds = employeeIds.filter((employeeId) => !pendingIds.has(employeeId)) if (availableEmployeeIds.length === 0) return if (checked) { const toCreateIds = availableEmployeeIds.filter((employeeId) => canCreateValidationRowFromAbsence(employeeId)) if (toCreateIds.length > 0) { await bulkUpsertWorkHours({ workDate: selectedDate.value, entries: toCreateIds.map((employeeId) => buildEmptyDriverEntry(employeeId)) }, { toast: false }) await loadWorkHours() } } const targetEmployeeIds = availableEmployeeIds.filter((employeeId) => canToggleValidation(employeeId)) if (targetEmployeeIds.length === 0) { toast.error({ title: 'Validation impossible', message: 'Aucune ligne ne peut être validée.' }) return } validatingRowIds.value = Array.from(new Set([...validatingRowIds.value, ...targetEmployeeIds])) try { const result = await bulkUpdateWorkHourValidation({ workDate: selectedDate.value, isValid: checked, employeeIds: targetEmployeeIds }, { toast: false }) await loadWorkHours() if (result.updated === 0) { toast.error({ title: 'Erreur', message: 'Aucune ligne mise à jour.' }) return } if (result.skipped > 0) { toast.success({ title: 'Succès partiel', message: `${result.updated} mise(s) à jour, ${result.skipped} ignorée(s).` }) return } toast.success({ title: 'Succès', message: checked ? `${result.updated} ligne(s) validée(s).` : `${result.updated} validation(s) retirée(s).` }) } catch { toast.error({ title: 'Erreur', message: 'Impossible de mettre à jour les validations.' }) } finally { validatingRowIds.value = validatingRowIds.value.filter((id) => !targetEmployeeIds.includes(id)) } } const toggleSiteValidationBulk = async (checked: boolean) => { if (!isSiteManager.value) return const employeeIds = bulkSiteValidatableEmployeeIds.value if (employeeIds.length === 0) return const pendingIds = new Set(siteValidatingRowIds.value) const availableEmployeeIds = employeeIds.filter((employeeId) => !pendingIds.has(employeeId)) if (availableEmployeeIds.length === 0) return if (checked) { const toCreateIds = availableEmployeeIds.filter((employeeId) => canCreateSiteValidationRowFromAbsence(employeeId)) if (toCreateIds.length > 0) { await bulkUpsertWorkHours({ workDate: selectedDate.value, entries: toCreateIds.map((employeeId) => buildEmptyDriverEntry(employeeId)) }, { toast: false }) await loadWorkHours() } } const targetEmployeeIds = availableEmployeeIds.filter((employeeId) => canToggleSiteValidation(employeeId)) if (targetEmployeeIds.length === 0) { toast.error({ title: 'Validation impossible', message: 'Aucune ligne ne peut être validée côté site.' }) return } siteValidatingRowIds.value = Array.from(new Set([...siteValidatingRowIds.value, ...targetEmployeeIds])) try { const result = await bulkUpdateWorkHourSiteValidation({ workDate: selectedDate.value, isSiteValid: checked, employeeIds: targetEmployeeIds }, { toast: false }) await loadWorkHours() if (result.updated === 0) { toast.error({ title: 'Erreur', message: 'Aucune ligne site mise à jour.' }) return } if (result.skipped > 0) { toast.success({ title: 'Succès partiel', message: `${result.updated} mise(s) à jour, ${result.skipped} ignorée(s).` }) return } toast.success({ title: 'Succès', message: checked ? `${result.updated} validation(s) site enregistrée(s).` : `${result.updated} validation(s) site retirée(s).` }) } catch { toast.error({ title: 'Erreur', message: 'Impossible de mettre à jour les validations site.' }) } finally { siteValidatingRowIds.value = siteValidatingRowIds.value.filter((id) => !targetEmployeeIds.includes(id)) } } const loadEmployees = async () => { const scopedEmployees = await listScopedEmployees() employees.value = sortEmployeesBySiteAndOrder(scopedEmployees) } const loadWorkHours = async () => { const workHours = await listWorkHoursByDate(selectedDate.value) hydrateRows(workHours) } const loadWeeklySummary = async () => { isWeekLoading.value = true try { weeklySummary.value = await getWeeklyWorkHourSummary(selectedDate.value) } finally { isWeekLoading.value = false } } const loadDayContext = async () => { dayContext.value = await getWorkHourDayContext(selectedDate.value) } const refreshByDate = async () => { if (isAdmin.value) { await Promise.all([loadWorkHours(), loadWeeklySummary(), loadDayContext(), loadAbsences()]) return } weeklySummary.value = null await Promise.all([loadWorkHours(), loadDayContext(), loadAbsences()]) } const loadPage = async () => { isLoading.value = true try { await loadPublicHolidaysForSelectedYear() await loadEmployees() await loadAbsenceTypes() await refreshByDate() } finally { isLoading.value = false } } onMounted(loadPage) watch(sites, (nextSites) => { const currentSiteIds = nextSites.map((site) => site.id) if (!sitesInitialized.value) { if (currentSiteIds.length === 0) return selectedSiteIds.value = currentSiteIds sitesInitialized.value = true return } selectedSiteIds.value = selectedSiteIds.value.filter((siteId) => currentSiteIds.includes(siteId)) }, { immediate: true }) watch(isAdmin, async (admin) => { if (!admin) { viewMode.value = 'day' weeklySummary.value = null await Promise.all([loadAbsenceTypes(), loadAbsences()]) return } await loadAbsenceTypes() await loadAbsences() }, { immediate: true }) watch(selectedDate, async () => { await loadPublicHolidaysForSelectedYear() await refreshByDate() }) const handleSave = async () => { if (isSubmitting.value || employees.value.length === 0) return isSubmitting.value = true try { const driverEmployees = employees.value.filter( (e) => e.isDriver === true && hasContractAtSelectedDate(e.id) ) const entries = driverEmployees.map((employee) => { const employeeId = employee.id const row = rows.value[employeeId] ?? emptyRow() const dayMin = toMinutes(row.dayHours) const nightMin = toMinutes(row.nightHours) const workshopMin = toMinutes(row.workshopHours) return { employeeId, morningFrom: null, morningTo: null, afternoonFrom: null, afternoonTo: null, eveningFrom: null, eveningTo: null, isPresentMorning: false, isPresentAfternoon: false, dayHoursMinutes: dayMin || null, nightHoursMinutes: nightMin || null, workshopHoursMinutes: workshopMin || null, hasBreakfast: row.hasBreakfast, hasLunch: row.hasLunch, hasDinner: row.hasDinner, hasOvernight: row.hasOvernight } }) if (entries.length === 0) return await bulkUpsertWorkHours({ workDate: selectedDate.value, entries }) await refreshByDate() } finally { isSubmitting.value = false } } return { isAdmin, isSelfUser, isSiteManager, viewMode, selectedDate, employeeFilter, sites, selectedSiteIds, employees, visibleEmployees, displayedEmployees, rows, absenceTypes, absenceForm, isAbsenceDrawerOpen, isAbsenceSubmitting, editingAbsence, weeklySummary, filteredWeeklySummary, isLoading, isWeekLoading, isSubmitting, dayGridCols, weekGridCols, saveButtonClass, formattedSelectedDate, isSelectedDateHoliday, weekDayHeaders, shortcutButtonClass, weekShortcutButtonClass, getWeekShortcutLabel, setToday, setYesterday, setTomorrow, setThisWeek, setPreviousWeek, setNextWeek, shiftDate, contractLabel, isRowLocked, hasContractAtSelectedDate, isValidationPending, isSiteValidationPending, canToggleValidation, canToggleSiteValidation, canCreateSiteValidationRowFromAbsence, isBulkValidationChecked, isBulkValidationIndeterminate, isBulkSiteValidationChecked, isBulkSiteValidationIndeterminate, canBulkToggleSiteValidation, toggleValidation, toggleSiteValidation, toggleValidationBulk, toggleSiteValidationBulk, getRowMetrics, getRowAbsenceLabel, getRowAbsenceStyle, getRowUpdatedAt, openAbsenceDrawer, submitAbsence, deleteAbsenceFromDrawer, closeAbsenceDrawer, formatMinutes, handleSave } }