fix : wip
This commit is contained in:
112
frontend/components/hours/HoursDayView.vue
Normal file
112
frontend/components/hours/HoursDayView.vue
Normal 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>
|
||||
144
frontend/components/hours/HoursToolbar.vue
Normal file
144
frontend/components/hours/HoursToolbar.vue
Normal 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>
|
||||
70
frontend/components/hours/HoursWeekView.vue
Normal file
70
frontend/components/hours/HoursWeekView.vue
Normal 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>
|
||||
12
frontend/components/hours/types.ts
Normal file
12
frontend/components/hours/types.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user