fix : wip

This commit is contained in:
2026-02-18 17:59:57 +01:00
parent 4256702add
commit c2e118dc33
47 changed files with 2689 additions and 345 deletions

View File

@@ -0,0 +1,112 @@
<template>
<div class="rounded-lg border border-neutral-200 bg-white overflow-hidden flex flex-1 min-h-0 flex-col">
<div class="overflow-y-auto min-h-0">
<div
class="grid w-full min-w-0 gap-1 border-b border-neutral-200 bg-tertiary-500 px-4 py-3 text-sm font-semibold text-neutral-700 sticky top-0 z-10"
:style="{ gridTemplateColumns: dayGridCols }"
>
<span>Nom</span>
<span class="pl-4">Début matin</span>
<span class="pr-2">Fin matin</span>
<span class="pl-2">Début après-midi</span>
<span class="pr-2">Fin après-midi</span>
<span class="pl-2">Début soir</span>
<span class="pr-2">Fin soir</span>
<span class="pl-2">Présent</span>
<span class="pl-2">Jour</span>
<span>Nuit</span>
<span>Total</span>
<span v-if="isAdmin">Valider</span>
</div>
<div
v-for="employee in employees"
:key="employee.id"
class="grid w-full min-w-0 items-center gap-1 border-b border-neutral-100 px-4 py-2 text-sm last:border-b-0"
:style="{ gridTemplateColumns: dayGridCols }"
>
<div class="text-neutral-900 min-w-0">
<p class="font-semibold truncate">
{{ employee.firstName }} {{ employee.lastName }}
<span class="font-normal text-neutral-600">({{ contractLabel(employee) }})</span>
</p>
<p class="text-neutral-500 truncate">{{ employee.site?.name ?? 'Sans site' }}</p>
</div>
<div class="pl-4">
<TimeSelect v-if="isTimeTracking(employee)" v-model="rows[employee.id].morningFrom" :disabled="isRowLocked(employee.id)"/>
<input
v-else-if="isPresenceTracking(employee)"
v-model="rows[employee.id].isPresentMorning"
type="checkbox"
class="cursor-pointer h-4 w-4"
:disabled="isRowLocked(employee.id)"
/>
</div>
<div class="pr-2">
<TimeSelect v-if="isTimeTracking(employee)" v-model="rows[employee.id].morningTo" :disabled="isRowLocked(employee.id)"/>
</div>
<div class="pl-2">
<TimeSelect v-if="isTimeTracking(employee)" v-model="rows[employee.id].afternoonFrom" :disabled="isRowLocked(employee.id)"/>
<input
v-else-if="isPresenceTracking(employee)"
v-model="rows[employee.id].isPresentAfternoon"
type="checkbox"
class="cursor-pointer h-4 w-4"
:disabled="isRowLocked(employee.id)"
/>
</div>
<div class="pr-2">
<TimeSelect v-if="isTimeTracking(employee)" v-model="rows[employee.id].afternoonTo" :disabled="isRowLocked(employee.id)"/>
</div>
<div class="pl-2">
<TimeSelect v-if="isTimeTracking(employee)" v-model="rows[employee.id].eveningFrom" :disabled="isRowLocked(employee.id)"/>
</div>
<div class="pr-2">
<TimeSelect v-if="isTimeTracking(employee)" v-model="rows[employee.id].eveningTo" :disabled="isRowLocked(employee.id)"/>
</div>
<div class="pl-2"></div>
<div class="pl-2 text-sm font-semibold text-neutral-700">
<div v-if="isTimeTracking(employee)">{{ formatMinutes(getRowMetrics(employee.id).dayMinutes) }}</div>
</div>
<div class="text-sm font-semibold text-neutral-700">
<div v-if="isTimeTracking(employee)">{{ formatMinutes(getRowMetrics(employee.id).nightMinutes) }}</div>
</div>
<div class="text-sm font-semibold text-neutral-700">
<div v-if="isTimeTracking(employee)">{{ formatMinutes(getRowMetrics(employee.id).totalMinutes) }}</div>
</div>
<div v-if="isAdmin">
<input
:checked="rows[employee.id]?.isValid ?? false"
type="checkbox"
class="h-4 w-4"
:class="rows[employee.id]?.workHourId ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'"
:disabled="!rows[employee.id]?.workHourId || isValidationPending(employee.id)"
@change="onToggleValidation(employee.id, ($event.target as HTMLInputElement).checked)"
/>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Employee } from '~/services/dto/employee'
import TimeSelect from '~/components/ui/TimeSelect.vue'
import type { HourRow } from './types'
const rows = defineModel<Record<number, HourRow>>('rows', { required: true })
defineProps<{
employees: Employee[]
isAdmin: boolean
dayGridCols: string
contractLabel: (employee: Employee) => string
isTimeTracking: (employee: Employee) => boolean
isPresenceTracking: (employee: Employee) => boolean
isRowLocked: (employeeId: number) => boolean
isValidationPending: (employeeId: number) => boolean
onToggleValidation: (employeeId: number, checked: boolean) => void
getRowMetrics: (employeeId: number) => { dayMinutes: number; nightMinutes: number; totalMinutes: number }
formatMinutes: (minutes: number) => string
}>()
</script>

View File

@@ -0,0 +1,144 @@
<template>
<div class="py-6 flex flex-col gap-3">
<SiteFilterSelector v-if="sites.length > 0" v-model="selectedSiteIds" :sites="sites" />
<div class="flex justify-between items-center gap-4">
<div class="flex gap-4 flex-wrap">
<div
v-if="viewMode === 'day'"
class="inline-flex h-10 w-[320px] overflow-hidden rounded-md border border-primary-500 bg-white"
>
<button
type="button"
class="flex-1 px-4 py-2 text-sm font-semibold active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40"
:class="shortcutButtonClass('yesterday')"
@click="emit('set-yesterday')"
>
Hier
</button>
<button
type="button"
class="flex-1 border-x border-primary-500 px-4 py-2 text-sm font-semibold active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40"
:class="shortcutButtonClass('today')"
@click="emit('set-today')"
>
Aujourd'hui
</button>
<button
type="button"
class="flex-1 px-4 py-2 text-sm font-semibold active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40"
:class="shortcutButtonClass('tomorrow')"
@click="emit('set-tomorrow')"
>
Demain
</button>
</div>
<div class="relative inline-flex h-10 w-[320px] items-center overflow-hidden rounded-md border border-primary-500 bg-white">
<input
ref="nativeDateInput"
v-model="selectedDate"
type="date"
class="pointer-events-none absolute inset-0 h-full w-full opacity-0"
tabindex="-1"
aria-hidden="true"
/>
<button
type="button"
class="h-10 px-3 text-lg font-semibold text-primary-500 hover:bg-tertiary-500 active:bg-tertiary-500 active:scale-[0.96] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40"
aria-label="Période précédente"
@click="emit('shift-date', -1)"
>
</button>
<button
type="button"
class="h-10 flex-1 border-x border-primary-500 px-4 text-sm font-semibold text-primary-500 text-center hover:bg-tertiary-500 active:bg-tertiary-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40"
@click="openDatePicker"
>
{{ formattedSelectedDate }}
</button>
<button
type="button"
class="h-10 px-3 text-lg font-semibold text-primary-500 hover:bg-tertiary-500 active:bg-tertiary-500 active:scale-[0.96] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40"
aria-label="Période suivante"
@click="emit('shift-date', 1)"
>
</button>
</div>
</div>
<div v-if="isAdmin" class="inline-flex h-10 overflow-hidden rounded-md border border-primary-500 bg-white">
<button
type="button"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-semibold active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40"
:class="viewModeButtonClass('day')"
@click="viewMode = 'day'"
>
<Icon name="mdi:calendar-clock" />
Jour
</button>
<button
type="button"
class="inline-flex items-center gap-2 border-l border-primary-500 px-4 py-2 text-sm font-semibold active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40"
:class="viewModeButtonClass('week')"
@click="viewMode = 'week'"
>
<Icon name="mdi:calendar-week" />
Semaine
</button>
</div>
</div>
<div class="w-80 max-w-full">
<EmployeeNameFilterInput v-model="employeeFilter" />
</div>
</div>
</template>
<script setup lang="ts">
import type { Site } from '~/services/dto/site'
import EmployeeNameFilterInput from '~/components/EmployeeNameFilterInput.vue'
import SiteFilterSelector from '~/components/SiteFilterSelector.vue'
const selectedDate = defineModel<string>('selectedDate', { required: true })
const viewMode = defineModel<'day' | 'week'>('viewMode', { required: true })
const selectedSiteIds = defineModel<number[]>('selectedSiteIds', { required: true })
const employeeFilter = defineModel<string>('employeeFilter', { required: true })
defineProps<{
isAdmin: boolean
sites: Site[]
formattedSelectedDate: string
shortcutButtonClass: (target: 'yesterday' | 'today' | 'tomorrow') => string
}>()
const emit = defineEmits<{
(e: 'set-yesterday'): void
(e: 'set-today'): void
(e: 'set-tomorrow'): void
(e: 'shift-date', value: number): void
}>()
const nativeDateInput = ref<HTMLInputElement | null>(null)
const viewModeButtonClass = (mode: 'day' | 'week') => {
if (viewMode.value === mode) {
return 'bg-primary-500 text-white'
}
return 'bg-white text-primary-500 hover:bg-tertiary-500'
}
const openDatePicker = () => {
const input = nativeDateInput.value
if (!input) return
if (typeof input.showPicker === 'function') {
input.showPicker()
return
}
input.focus()
input.click()
}
</script>

View File

@@ -0,0 +1,70 @@
<template>
<div class="rounded-lg border border-neutral-200 bg-white overflow-hidden flex flex-1 min-h-0 flex-col">
<div v-if="isWeekLoading" class="p-6 text-md text-neutral-600">Chargement de la semaine...</div>
<div v-else class="overflow-y-auto min-h-0">
<div
class="grid w-full min-w-0 gap-1 border-b border-neutral-200 bg-tertiary-500 px-4 py-3 text-sm font-semibold text-neutral-700 sticky top-0 z-10"
:style="{ gridTemplateColumns: weekGridCols }"
>
<span>Nom</span>
<span v-for="day in weekDayHeaders" :key="day.date" class="text-left">{{ day.label }}</span>
<span>Jour/Nuit sem.</span>
<span>Total sem.</span>
<span>+25%</span>
<span>+50%</span>
</div>
<div
v-for="row in weeklySummary?.rows ?? []"
:key="row.employeeId"
class="grid w-full min-w-0 items-center gap-1 border-b border-neutral-100 px-4 py-2 text-sm last:border-b-0"
:style="{ gridTemplateColumns: weekGridCols }"
>
<div class="text-neutral-900 min-w-0">
<p class="font-semibold truncate">
{{ row.firstName }} {{ row.lastName }}
<span class="font-normal text-neutral-600">({{ row.contractName ?? '-' }})</span>
</p>
<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">
<template v-if="row.trackingMode === 'PRESENCE'">{{ daily.present ?? 0 }}</template>
<template v-else>
<div>J {{ formatMinutes(daily.dayMinutes) }}</div>
<div>N {{ formatMinutes(daily.nightMinutes) }}</div>
</template>
</div>
<div class="font-semibold leading-4">
<template v-if="row.trackingMode === 'PRESENCE'">-</template>
<template v-else>
<div>J {{ formatMinutes(row.weeklyDayMinutes) }}</div>
<div>N {{ formatMinutes(row.weeklyNightMinutes) }}</div>
</template>
</div>
<div class="font-semibold">
{{ row.trackingMode === 'PRESENCE' ? (row.weeklyPresenceCount ?? 0) : formatMinutes(row.weeklyTotalMinutes) }}
</div>
<div class="font-semibold">
{{ row.trackingMode === 'PRESENCE' ? '-' : formatMinutes(row.weeklyOvertime25Minutes ?? 0) }}
</div>
<div class="font-semibold">
{{ row.trackingMode === 'PRESENCE' ? '-' : formatMinutes(row.weeklyOvertime50Minutes ?? 0) }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { WeeklyWorkHourSummary } from '~/services/dto/work-hour'
defineProps<{
isWeekLoading: boolean
weekGridCols: string
weeklySummary: WeeklyWorkHourSummary | null
weekDayHeaders: Array<{ date: string; label: string }>
formatMinutes: (minutes: number) => string
}>()
</script>

View File

@@ -0,0 +1,12 @@
export type HourRow = {
workHourId: number | null
morningFrom: string
morningTo: string
afternoonFrom: string
afternoonTo: string
eveningFrom: string
eveningTo: string
isPresentMorning: boolean
isPresentAfternoon: boolean
isValid: boolean
}