fix : wip

This commit is contained in:
2026-02-19 17:44:37 +01:00
parent c2e118dc33
commit 13274ff297
31 changed files with 1539 additions and 126 deletions

View File

@@ -1,11 +1,17 @@
import { computed, onMounted, ref, watch } from 'vue'
import type { Employee } from '~/services/dto/employee'
import type { Site } from '~/services/dto/site'
import type { WorkHour, WeeklyWorkHourSummary } from '~/services/dto/work-hour'
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 { HourRow } from '~/components/hours/types'
import { listScopedEmployees } from '~/services/employees'
import { listAbsenceTypes } from '~/services/absence-types'
import { createAbsence, deleteAbsence, listAbsences, updateAbsence } from '~/services/absences'
import {
bulkUpsertWorkHours,
getWorkHourDayContext,
getWeeklyWorkHourSummary,
listWorkHoursByDate,
updateWorkHourValidation
@@ -14,7 +20,9 @@ import {
formatDateLongFr,
formatWeekDayHeaderFr,
formatWeekRangeFr,
getIsoWeekNumber,
getOffsetFromTodayYmd,
getWeekStartDate,
getTodayYmd,
parseYmd,
shiftYmd
@@ -23,6 +31,7 @@ import { sortEmployeesBySiteAndOrder } from '~/utils/employee'
export const useHoursPage = () => {
const auth = useAuthStore()
const toast = useToast()
const isAdmin = computed(() => auth.user?.roles?.includes('ROLE_ADMIN') ?? false)
const viewMode = ref<'day' | 'week'>('day')
@@ -32,19 +41,33 @@ export const useHoursPage = () => {
const selectedSiteIds = ref<number[]>([])
const sitesInitialized = ref(false)
const rows = ref<Record<number, HourRow>>({})
const dayContext = ref<WorkHourDayContext | null>(null)
const weeklySummary = ref<WeeklyWorkHourSummary | null>(null)
const absenceTypes = ref<AbsenceType[]>([])
const absences = ref<Absence[]>([])
const isAbsenceDrawerOpen = ref(false)
const isAbsenceSubmitting = ref(false)
const editingAbsence = ref<Absence | null>(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<number[]>([])
const dayGridCols = computed(() => {
const metricCol = '0.5fr'
const cols = `1.2fr repeat(6, 1fr) ${metricCol} ${metricCol} ${metricCol} ${metricCol}`
return isAdmin.value ? `${cols} ${metricCol}` : cols
const metricCol = '0.4fr'
return `1.2fr repeat(6, 1fr) 0.8fr ${metricCol} ${metricCol} ${metricCol} ${metricCol}`
})
const weekGridCols = '1.6fr repeat(7, 1fr) 1fr 0.8fr 0.8fr 0.8fr'
const weekGridCols = '1.6fr repeat(7, 1fr) repeat(6, 0.6fr)'
const sites = computed<Site[]>(() => {
const siteMap = new Map<number, Site>()
@@ -94,6 +117,34 @@ export const useHoursPage = () => {
})
const isValidationPending = (employeeId: number) => validatingRowIds.value.includes(employeeId)
const canToggleValidation = (employeeId: number) => !!rows.value[employeeId]?.workHourId
const validatableEmployeeIds = computed(() => {
return employees.value
.map((employee) => employee.id)
.filter((employeeId) => canToggleValidation(employeeId))
})
const isBulkValidationChecked = computed(() => {
const ids = validatableEmployeeIds.value
if (ids.length === 0) return false
return ids.every((employeeId) => rows.value[employeeId]?.isValid ?? false)
})
const isBulkValidationIndeterminate = computed(() => {
const ids = validatableEmployeeIds.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 dayContextByEmployeeId = computed(() => {
const map = new Map<number, WorkHourDayContext['rows'][number]>()
for (const row of dayContext.value?.rows ?? []) {
map.set(row.employeeId, row)
}
return map
})
const shortcutButtonClass = (target: 'yesterday' | 'today' | 'tomorrow') => {
const targetDate = target === 'yesterday'
@@ -109,6 +160,37 @@ export const useHoursPage = () => {
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
@@ -146,6 +228,40 @@ export const useHoursPage = () => {
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 emptyRow = (): HourRow => ({
workHourId: null,
morningFrom: '',
@@ -243,10 +359,42 @@ export const useHoursPage = () => {
nightMinutes += nightIntervalMinutes(from, to)
}
const creditedMinutes = dayContextByEmployeeId.value.get(employeeId)?.creditedMinutes ?? 0
totalMinutes += creditedMinutes
const dayMinutes = Math.max(0, totalMinutes - nightMinutes)
return { dayMinutes, nightMinutes, totalMinutes }
}
const getRowAbsenceLabel = (employeeId: number) => {
const dayRow = dayContextByEmployeeId.value.get(employeeId)
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 getPresenceDayValue = (employeeId: number) => {
const row = rows.value[employeeId]
const basePresence = (row?.isPresentMorning ? 0.5 : 0) + (row?.isPresentAfternoon ? 0.5 : 0)
const creditedPresence = dayContextByEmployeeId.value.get(employeeId)?.creditedPresenceUnits ?? 0
const total = Math.min(1, basePresence + creditedPresence)
return Number.isInteger(total) ? String(total) : total.toFixed(1)
}
const isHalfLockedByAbsence = (employeeId: number, half: 'AM' | 'PM') => {
const dayRow = dayContextByEmployeeId.value.get(employeeId)
if (!dayRow) return false
return half === 'AM' ? dayRow.absentMorning : dayRow.absentAfternoon
}
const isEveningLockedByAbsence = (employeeId: number) => {
const dayRow = dayContextByEmployeeId.value.get(employeeId)
if (!dayRow) return false
return dayRow.absentMorning && dayRow.absentAfternoon
}
const formatMinutes = (minutes: number) => {
const safeMinutes = Math.max(0, minutes)
const hours = Math.floor(safeMinutes / 60)
@@ -280,19 +428,162 @@ export const useHoursPage = () => {
rows.value = nextRows
}
const toggleValidation = async (employeeId: number, checked: boolean) => {
const loadAbsenceTypes = async () => {
absenceTypes.value = await listAbsenceTypes()
}
const loadAbsences = async () => {
absences.value = await listAbsences({
from: selectedDate.value,
to: selectedDate.value,
siteIds: isAdmin.value ? selectedSiteIds.value : undefined
})
}
const openAbsenceDrawer = (employeeId: number) => {
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 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 refreshByDate()
await loadAbsences()
} finally {
isAbsenceSubmitting.value = false
}
}
const deleteAbsenceFromDrawer = async () => {
if (!editingAbsence.value || isAbsenceSubmitting.value) return
isAbsenceSubmitting.value = true
try {
await deleteAbsence(editingAbsence.value.id)
closeAbsenceDrawer()
await refreshByDate()
await loadAbsences()
} finally {
isAbsenceSubmitting.value = false
}
}
const toggleValidation = async (
employeeId: number,
checked: boolean,
options: { toast?: boolean } = {}
) => {
const row = rows.value[employeeId]
if (!row?.workHourId || isValidationPending(employeeId)) return
validatingRowIds.value = [...validatingRowIds.value, employeeId]
try {
await updateWorkHourValidation(row.workHourId, checked)
await updateWorkHourValidation(row.workHourId, checked, { toast: options.toast })
row.isValid = checked
} finally {
validatingRowIds.value = validatingRowIds.value.filter((id) => id !== employeeId)
}
}
const toggleValidationBulk = async (checked: boolean) => {
const employeeIds = validatableEmployeeIds.value
if (employeeIds.length === 0) return
let successCount = 0
let failedCount = 0
for (const employeeId of employeeIds) {
if (isValidationPending(employeeId)) continue
try {
await toggleValidation(employeeId, checked, { toast: false })
successCount += 1
} catch {
failedCount += 1
}
}
if (failedCount === 0) {
toast.success({
title: 'Succès',
message: checked
? `${successCount} ligne(s) validée(s).`
: `${successCount} validation(s) retirée(s).`
})
return
}
if (successCount === 0) {
toast.error({
title: 'Erreur',
message: 'Impossible de mettre à jour les validations.'
})
return
}
toast.error({
title: 'Erreur',
message: `${successCount} mise(s) à jour, ${failedCount} en échec.`
})
}
const loadEmployees = async () => {
const scopedEmployees = await listScopedEmployees()
employees.value = sortEmployeesBySiteAndOrder(scopedEmployees)
@@ -312,20 +603,25 @@ export const useHoursPage = () => {
}
}
const loadDayContext = async () => {
dayContext.value = await getWorkHourDayContext(selectedDate.value)
}
const refreshByDate = async () => {
if (isAdmin.value) {
await Promise.all([loadWorkHours(), loadWeeklySummary()])
await Promise.all([loadWorkHours(), loadWeeklySummary(), loadDayContext(), loadAbsences()])
return
}
weeklySummary.value = null
await loadWorkHours()
await Promise.all([loadWorkHours(), loadDayContext(), loadAbsences()])
}
const loadPage = async () => {
isLoading.value = true
try {
await loadEmployees()
await loadAbsenceTypes()
await refreshByDate()
} finally {
isLoading.value = false
@@ -347,11 +643,15 @@ export const useHoursPage = () => {
selectedSiteIds.value = selectedSiteIds.value.filter((siteId) => currentSiteIds.includes(siteId))
}, { immediate: true })
watch(isAdmin, (admin) => {
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 () => {
@@ -414,6 +714,11 @@ export const useHoursPage = () => {
employees,
visibleEmployees,
rows,
absenceTypes,
absenceForm,
isAbsenceDrawerOpen,
isAbsenceSubmitting,
editingAbsence,
weeklySummary,
filteredWeeklySummary,
isLoading,
@@ -425,17 +730,34 @@ export const useHoursPage = () => {
formattedSelectedDate,
weekDayHeaders,
shortcutButtonClass,
weekShortcutButtonClass,
getWeekShortcutLabel,
setToday,
setYesterday,
setTomorrow,
setThisWeek,
setPreviousWeek,
setNextWeek,
shiftDate,
contractLabel,
isTimeTracking,
isPresenceTracking,
isRowLocked,
isHalfLockedByAbsence,
isEveningLockedByAbsence,
isValidationPending,
canToggleValidation,
isBulkValidationChecked,
isBulkValidationIndeterminate,
toggleValidation,
toggleValidationBulk,
getRowMetrics,
getRowAbsenceLabel,
getPresenceDayValue,
openAbsenceDrawer,
submitAbsence,
deleteAbsenceFromDrawer,
closeAbsenceDrawer,
formatMinutes,
handleSave
}