fix : affichage des absences dans les heures vue semaine + refacto

This commit is contained in:
2026-02-20 12:20:34 +01:00
parent d9fa301159
commit fb52be128e
21 changed files with 513 additions and 112 deletions

View File

@@ -126,11 +126,23 @@
<div v-if="isAdmin" class="w-80 max-w-full">
<EmployeeNameFilterInput v-model="employeeFilter" />
</div>
<div
v-if="isAdmin && viewMode === 'week' && absenceTypes.length > 0"
class="flex flex-wrap items-center gap-6"
>
<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>
</template>
<script setup lang="ts">
import type { Site } from '~/services/dto/site'
import type { AbsenceType } from '~/services/dto/absence-type'
import EmployeeNameFilterInput from '~/components/EmployeeNameFilterInput.vue'
import SiteFilterSelector from '~/components/SiteFilterSelector.vue'
import { weekInputValueToYmd, ymdToWeekInputValue } from '~/utils/date'
@@ -143,6 +155,7 @@ const employeeFilter = defineModel<string>('employeeFilter', { required: true })
defineProps<{
isAdmin: boolean
sites: Site[]
absenceTypes: AbsenceType[]
formattedSelectedDate: string
shortcutButtonClass: (target: 'yesterday' | 'today' | 'tomorrow') => string
weekShortcutButtonClass: (target: 'previousWeek' | 'thisWeek' | 'nextWeek') => string

View File

@@ -30,7 +30,14 @@
<p class="text-[11px] text-neutral-500 truncate">{{ row.siteName ?? 'Sans site' }}</p>
</div>
<div v-for="daily in row.daily" :key="daily.date" class="text-left leading-4">
<div
v-for="daily in row.daily"
:key="daily.date"
class="text-left leading-4 rounded-md px-2 py-1"
:class="daily.hasAbsence ? 'text-white' : ''"
:style="getDailyCellStyle(daily)"
:title="daily.absenceLabel ?? ''"
>
<template v-if="row.trackingMode === 'PRESENCE'">{{ daily.present ?? 0 }}</template>
<template v-else>
<div>J {{ formatMinutes(daily.dayMinutes) }}</div>
@@ -52,13 +59,13 @@
{{ row.trackingMode === 'PRESENCE' ? '-' : formatMinutes(row.weeklyOvertimeTotalMinutes ?? 0) }}
</div>
<div class="font-semibold">
{{ row.trackingMode === 'PRESENCE' ? '-' : formatMinutes(row.weeklyOvertime25Minutes ?? 0) }}
{{ row.trackingMode === 'PRESENCE' || isInterimContract(row.contractType) ? '-' : formatMinutes(row.weeklyOvertime25Minutes ?? 0) }}
</div>
<div class="font-semibold">
{{ row.trackingMode === 'PRESENCE' ? '-' : formatMinutes(row.weeklyOvertime50Minutes ?? 0) }}
{{ row.trackingMode === 'PRESENCE' || isInterimContract(row.contractType) ? '-' : formatMinutes(row.weeklyOvertime50Minutes ?? 0) }}
</div>
<div class="font-semibold">
{{ row.trackingMode === 'PRESENCE' ? '-' : formatMinutes(row.weeklyRecoveryMinutes ?? 0) }}
{{ row.trackingMode === 'PRESENCE' || isInterimContract(row.contractType) ? '-' : formatMinutes(row.weeklyRecoveryMinutes ?? 0) }}
</div>
</div>
</div>
@@ -67,6 +74,19 @@
<script setup lang="ts">
import type { WeeklyWorkHourSummary } from '~/services/dto/work-hour'
import { CONTRACT_TYPES, type ContractType } from '~/services/dto/contract'
const isInterimContract = (contractType?: ContractType | null) => {
return contractType === CONTRACT_TYPES.INTERIM
}
const getDailyCellStyle = (daily: {
hasAbsence?: boolean
absenceColor?: string | null
}) => {
if (!daily.hasAbsence) return undefined
return { backgroundColor: daily.absenceColor || '#dc2626' }
}
defineProps<{
isWeekLoading: boolean

View File

@@ -5,6 +5,7 @@ import type { WorkHour, WorkHourDayContext, WeeklyWorkHourSummary } from '~/serv
import type { AbsenceType } from '~/services/dto/absence-type'
import type { Absence } from '~/services/dto/absence'
import type { HalfDay } from '~/services/dto/half-day'
import { CONTRACT_TYPES, TRACKING_MODES } from '~/services/dto/contract'
import type { HourRow } from '~/components/hours/types'
import { listScopedEmployees } from '~/services/employees'
import { listAbsenceTypes } from '~/services/absence-types'
@@ -275,14 +276,17 @@ export const useHoursPage = () => {
isValid: false
})
const isPresenceTracking = (employee: Employee) => employee.contract?.trackingMode === 'PRESENCE'
const isPresenceTracking = (employee: Employee) => employee.contract?.trackingMode === TRACKING_MODES.PRESENCE
const isTimeTracking = (employee: Employee) => !isPresenceTracking(employee)
const isRowLocked = (employeeId: number) => rows.value[employeeId]?.isValid ?? false
const contractLabel = (employee: Employee) => {
const contract = employee.contract
if (!contract) return '-'
if (contract.weeklyHours !== null && contract.weeklyHours !== undefined && contract.trackingMode === 'TIME') {
if (contract.type === CONTRACT_TYPES.INTERIM) {
return contract.name
}
if (contract.weeklyHours !== null && contract.weeklyHours !== undefined && contract.trackingMode === TRACKING_MODES.TIME) {
return `${contract.weeklyHours}h`
}
return contract.name
@@ -392,7 +396,7 @@ export const useHoursPage = () => {
const isEveningLockedByAbsence = (employeeId: number) => {
const dayRow = dayContextByEmployeeId.value.get(employeeId)
if (!dayRow) return false
return dayRow.absentMorning && dayRow.absentAfternoon
return dayRow.absentAfternoon
}
const formatMinutes = (minutes: number) => {
@@ -475,6 +479,42 @@ export const useHoursPage = () => {
isAbsenceDrawerOpen.value = true
}
const applyLocalClearFromAbsence = (employeeId: number, startHalf: HalfDay, endHalf: HalfDay) => {
const row = rows.value[employeeId]
if (!row) return
if (startHalf === 'AM' && endHalf === 'AM') {
row.morningFrom = ''
row.morningTo = ''
return
}
if (startHalf === 'PM' && endHalf === 'PM') {
row.afternoonFrom = ''
row.afternoonTo = ''
row.eveningFrom = ''
row.eveningTo = ''
return
}
row.morningFrom = ''
row.morningTo = ''
row.afternoonFrom = ''
row.afternoonTo = ''
row.eveningFrom = ''
row.eveningTo = ''
}
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
@@ -504,9 +544,9 @@ export const useHoursPage = () => {
})
}
applyLocalClearFromAbsence(Number(form.employeeId), form.startHalf, form.endHalf)
closeAbsenceDrawer()
await refreshByDate()
await loadAbsences()
await refreshAfterAbsenceChange()
} finally {
isAbsenceSubmitting.value = false
}
@@ -519,8 +559,7 @@ export const useHoursPage = () => {
try {
await deleteAbsence(editingAbsence.value.id)
closeAbsenceDrawer()
await refreshByDate()
await loadAbsences()
await refreshAfterAbsenceChange()
} finally {
isAbsenceSubmitting.value = false
}

View File

@@ -6,17 +6,10 @@
<img src="/malio.png" alt="Logo" class="w-auto"/>
</div>
<nav class="flex-1 px-4 pb-6">
<NuxtLink
to="/hours"
class="flex items-center gap-3 px-4 pb-3 pt-6 text-md font-semibold text-neutral-700 hover:bg-tertiary-500 hover:text-primary-500 border-t border-secondary-500"
active-class="bg-tertiary-500 text-primary-500"
>
Heures
</NuxtLink>
<template v-if="isAdmin">
<NuxtLink
to="/"
class="flex items-center gap-3 px-4 py-3 text-md font-semibold text-neutral-700 hover:bg-tertiary-500 hover:text-primary-500"
class="flex items-center gap-3 px-4 pb-3 pt-6 text-md font-semibold text-neutral-700 hover:bg-tertiary-500 hover:text-primary-500 border-t border-secondary-500"
active-class="bg-tertiary-500 text-primary-500"
>
Tableau de bord
@@ -28,6 +21,15 @@
>
Calendrier
</NuxtLink>
</template>
<NuxtLink
to="/hours"
class="flex items-center gap-3 px-4 py-3 text-md font-semibold text-neutral-700 hover:bg-tertiary-500 hover:text-primary-500"
active-class="bg-tertiary-500 text-primary-500"
>
Heures
</NuxtLink>
<template v-if="isAdmin">
<NuxtLink
to="/employees"
class="flex items-center gap-3 px-4 py-3 text-md font-semibold text-neutral-700 hover:bg-tertiary-500 hover:text-primary-500"

View File

@@ -11,6 +11,7 @@
v-model:employee-filter="employeeFilter"
:is-admin="isAdmin"
:sites="sites"
:absence-types="absenceTypes"
:formatted-selected-date="formattedSelectedDate"
:shortcut-button-class="shortcutButtonClass"
:week-shortcut-button-class="weekShortcutButtonClass"

View File

@@ -1,7 +1,25 @@
export const TRACKING_MODES = {
TIME: 'TIME',
PRESENCE: 'PRESENCE'
} as const
export type TrackingMode = (typeof TRACKING_MODES)[keyof typeof TRACKING_MODES]
export const CONTRACT_TYPES = {
FORFAIT: 'FORFAIT',
H35: '35H',
H39: '39H',
INTERIM: 'INTERIM',
CUSTOM: 'CUSTOM'
} as const
export type ContractType = (typeof CONTRACT_TYPES)[keyof typeof CONTRACT_TYPES]
export type Contract = {
id: number
name: string
trackingMode: 'TIME' | 'PRESENCE'
trackingMode: TrackingMode
type: ContractType
weeklyHours?: number | null
isActive?: boolean
}

View File

@@ -1,4 +1,5 @@
import type { Employee } from './employee'
import type { ContractType, TrackingMode } from './contract'
export type WorkHour = {
id: number
@@ -33,6 +34,9 @@ export type WeeklyWorkHourDailySummary = {
nightMinutes: number
totalMinutes: number
present?: number | null
hasAbsence?: boolean
absenceLabel?: string | null
absenceColor?: string | null
}
export type WeeklyWorkHourRowSummary = {
@@ -41,7 +45,8 @@ export type WeeklyWorkHourRowSummary = {
lastName: string
siteName?: string | null
contractName?: string | null
trackingMode?: 'TIME' | 'PRESENCE' | null
contractType?: ContractType | null
trackingMode?: TrackingMode | null
daily: WeeklyWorkHourDailySummary[]
weeklyDayMinutes: number
weeklyNightMinutes: number