Files
SIRH/frontend/components/CalendarGrid.vue
tristan ee16779777
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
[#322] Page horaire (#4)
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|        #322          |        Page horaire         |

## Description de la PR
[#322] Page horaire

## Modification du .env

## Check list

- [ ] Pas de régression
- [ ] TU/TI/TF rédigée
- [ ] TU/TI/TF OK
- [ ] CHANGELOG modifié

Reviewed-on: #4
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-02-20 11:23:52 +00:00

189 lines
8.5 KiB
Vue

<template>
<div class="h-full min-h-0 overflow-auto rounded-lg border border-neutral-200 bg-white">
<div class="min-w-[900px]">
<div class="grid" :style="gridStyle" @mouseleave="clearHoveredCell">
<div
class="sticky left-0 top-0 z-30 border-b border-neutral-200 bg-tertiary-500 px-4 py-3 text-md font-semibold text-neutral-700"
>
Employés
</div>
<div
v-for="day in daysInMonth"
:key="day.date"
class="sticky top-0 z-20 border-b border-neutral-200 px-2 py-3 text-center text-xs font-semibold transition-colors"
:class="isHoveredColumn(day.date) ? 'bg-primary-500 text-white' : 'bg-tertiary-500 text-neutral-700'"
>
<div>{{ day.label }}</div>
<div
class="text-[10px]"
:class="isHoveredColumn(day.date) ? 'text-white/90' : 'text-neutral-500'"
>
{{ day.weekday }}
</div>
</div>
<template v-for="employee in visibleEmployees" :key="employee.id">
<div
class="sticky left-0 z-10 border-b border-neutral-100 px-4 py-3 text-md font-semibold text-black cursor-pointer transition-shadow"
:class="isHoveredRow(employee.id) ? 'bg-primary-500 text-white ring-2 ring-inset ring-primary-500/40' : ''"
:style="rowHeaderStyle(employee)"
draggable="true"
@dragstart="handleDragStart($event, employee)"
@dragover="handleDragOver"
@drop="handleDrop($event, employee)"
>
{{ formatEmployeeName(employee) }}
</div>
<div
v-for="day in daysInMonth"
:key="employee.id + '-' + day.date"
class="border-b border-neutral-300 px-2 py-2 text-center text-xs text-neutral-800 transition-colors"
:class="cellContainerClass(employee.id, day.date)"
@mouseenter="setHoveredCell(employee.id, day.date)"
>
<template v-if="getCellInfo(employee.id, day.date)">
<button
type="button"
class="relative flex h-8 w-full items-center justify-center overflow-hidden rounded-md border border-neutral-200 text-[11px] font-semibold text-neutral-800"
:class="isHolidayDate(day.date) ? 'cursor-not-allowed opacity-80' : ''"
:style="getCellStyle(employee.id, day.date)"
:disabled="isHolidayDate(day.date)"
@click="handleCellClick(employee, day.date)"
>
<span v-if="!getCellInfo(employee.id, day.date)?.halfLabel">
{{ getCellInfo(employee.id, day.date)?.code }}
</span>
<template v-else>
<span
v-if="getCellInfo(employee.id, day.date)?.halfLabel === 'AM'"
class="absolute top-0 left-0 flex h-1/2 w-full items-center justify-center text-[10px] font-semibold"
>
{{ getCellInfo(employee.id, day.date)?.code }}
</span>
<span
v-else
class="absolute bottom-0 left-0 flex h-1/2 w-full items-center justify-center text-[10px] font-semibold"
>
{{ getCellInfo(employee.id, day.date)?.code }}
</span>
</template>
</button>
</template>
<template v-else>
<button
type="button"
class="relative flex h-8 w-full items-center justify-center rounded-md border border-neutral-200 text-[11px] font-semibold text-neutral-800 bg-white"
:class="isHolidayDate(day.date) ? 'cursor-not-allowed opacity-80' : ''"
:style="getCellStyle(employee.id, day.date)"
:disabled="isHolidayDate(day.date)"
@click="handleCellClick(employee, day.date)"
>
<span></span>
</button>
</template>
</div>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Employee } from '~/services/dto/employee'
import type { HalfDay } from '~/services/dto/half-day'
type DayInfo = {
date: string
label: string
weekday: string
}
const props = defineProps<{
daysInMonth: DayInfo[]
visibleEmployees: Employee[]
gridStyle: Record<string, string>
getCellStyle: (employeeId: number, date: string) => Record<string, string> | undefined
getCellInfo: (employeeId: number, date: string) => { id: number; code: string; color: string; halfLabel?: HalfDay; textColor?: string } | null
formatEmployeeName: (employee: Employee) => string
isHolidayDate: (date: string) => boolean
}>()
const emit = defineEmits<{
(event: 'cell-click', employee: Employee, date: string): void
(event: 'reorder', payload: { dragId: number; dropId: number }): void
}>()
const handleCellClick = (employee: Employee, date: string) => {
emit('cell-click', employee, date)
}
const handleDragStart = (event: DragEvent, employee: Employee) => {
if (!event.dataTransfer) return
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.setData('text/plain', String(employee.id))
}
const handleDragOver = (event: DragEvent) => {
event.preventDefault()
}
const handleDrop = (event: DragEvent, employee: Employee) => {
event.preventDefault()
const dragId = Number(event.dataTransfer?.getData('text/plain'))
if (!dragId || dragId === employee.id) return
emit('reorder', { dragId, dropId: employee.id })
}
// Etat de la cellule actuellement survolee.
const hoveredEmployeeId = ref<number | null>(null)
const hoveredDate = ref<string | null>(null)
const setHoveredCell = (employeeId: number, date: string) => {
hoveredEmployeeId.value = employeeId
hoveredDate.value = date
}
const clearHoveredCell = () => {
hoveredEmployeeId.value = null
hoveredDate.value = null
}
const isHoveredRow = (employeeId: number) => hoveredEmployeeId.value === employeeId
const isHoveredColumn = (date: string) => hoveredDate.value === date
// On garde la couleur du site tant que la ligne n'est pas survolee.
const rowHeaderStyle = (employee: Employee) => {
if (isHoveredRow(employee.id)) return undefined
return { backgroundColor: employee.site?.color ?? '#304998' }
}
// Index de ligne par employe pour savoir si une case est "au-dessus" de la case survolee.
const employeeIndexById = computed(() => {
const indexMap = new Map<number, number>()
props.visibleEmployees.forEach((employee, index) => {
indexMap.set(employee.id, index)
})
return indexMap
})
const cellContainerClass = (employeeId: number, date: string) => {
if (!hoveredEmployeeId.value || !hoveredDate.value) return 'hover:bg-primary-500'
const hoveredRowIndex = employeeIndexById.value.get(hoveredEmployeeId.value)
const currentRowIndex = employeeIndexById.value.get(employeeId)
// Forme en L:
// - ligne: toutes les cases a gauche (et la case cible)
// - colonne: toutes les cases au-dessus (et la case cible)
const isOnLeftSegment = isHoveredRow(employeeId) && date <= hoveredDate.value
const isOnTopSegment = isHoveredColumn(date)
&& typeof hoveredRowIndex === 'number'
&& typeof currentRowIndex === 'number'
&& currentRowIndex <= hoveredRowIndex
if (isOnLeftSegment || isOnTopSegment) return 'bg-primary-500'
return 'hover:bg-primary-500'
}
</script>