Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #6 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
310 lines
12 KiB
Vue
310 lines
12 KiB
Vue
<template>
|
|
<section class="flex h-full min-h-0 flex-col overflow-hidden pt-8">
|
|
<div class="grid grid-cols-4 rounded-md bg-primary-500 text-white text-[20]">
|
|
<div class="flex flex-col gap-2 jutify-center items-center border-r-4 border-white py-3">
|
|
<p><strong class="uppercase font-semibold">Année acquis :</strong> {{
|
|
formatCount(summary?.acquiredDays)
|
|
}} Jours</p>
|
|
<p><strong class="uppercase font-semibold">Reste à prendre :</strong>
|
|
{{ formatCount(summary?.remainingDays) }} Jours</p>
|
|
</div>
|
|
<div class="flex flex-col gap-2 jutify-center items-center border-r-4 border-white py-3">
|
|
<p><span class="uppercase font-semibold">Samedi acquis :</span>
|
|
{{ formatCount(summary?.acquiredSaturdays) }} Jours</p>
|
|
<p><span class="uppercase font-semibold">Reste à prendre :</span>
|
|
{{ formatCount(summary?.remainingSaturdays) }} Jours</p>
|
|
</div>
|
|
<div class="flex flex-col gap-2 jutify-center items-center border-r-4 border-white py-3">
|
|
<p><span class="uppercase font-semibold">Fractionné acquis : </span>{{ formatCount(summary?.fractionedDays) }} Jours</p>
|
|
<button
|
|
class="flex justify-center items-center gap-2 bg-white text-primary-500 font-bold w-[150px] rounded-md py-[1px]"
|
|
@click="openFractionedDrawer"
|
|
>
|
|
{{ summary?.fractionedDays === 0 ? '+ Ajouter' : 'Modifier' }}</button>
|
|
</div>
|
|
<div class="flex flex-col jutify-center gap-2 items-center py-3">
|
|
<p><span class="uppercase font-semibold">En cours d'acquisition :</span></p>
|
|
<p>{{ formatCount(summary?.accruingDays) }} Jours</p>
|
|
</div>
|
|
</div>
|
|
<div class="mt-8 min-h-0 flex-1 overflow-y-auto pr-2">
|
|
<div class="grid grid-cols-4 gap-10">
|
|
<div v-for="month in months" :key="month.label" class="rounded-md bg-tertiary-500 text-primary-500">
|
|
<div class="flex justify-center rounded-t-md bg-primary-500 py-1 font-bold uppercase text-white">
|
|
{{ month.label }}
|
|
</div>
|
|
<div class="grid grid-cols-7 gap-1 px-2 py-2 text-center text-md font-bold">
|
|
<p v-for="weekday in weekDayLabels" :key="weekday">{{ weekday }}</p>
|
|
</div>
|
|
<div class="grid grid-cols-7 gap-4 px-2 pb-2 text-center text-md">
|
|
<template v-for="(day, index) in month.cells" :key="`${month.label}-${index}`">
|
|
<div v-if="!day" class="h-6"/>
|
|
<div
|
|
v-else
|
|
class="flex items-center justify-center"
|
|
>
|
|
<div
|
|
class="h-6 w-6"
|
|
:class="getDayClass(day)"
|
|
:style="getDayStyle(day)"
|
|
:title="getDayTitle(day)"
|
|
>
|
|
{{ getDayText(day) }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<AppDrawer v-model="isFractionedDrawerOpen" title="Jours fractionnés">
|
|
<form class="space-y-4" @submit.prevent="handleSubmitFractioned">
|
|
<div>
|
|
<label class="text-md font-semibold text-neutral-700" for="fractioned-days">
|
|
Nombre de jours <span class="text-red-600">*</span>
|
|
</label>
|
|
<input
|
|
id="fractioned-days"
|
|
v-model="fractionedForm.days"
|
|
type="number"
|
|
step="0.5"
|
|
min="0"
|
|
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-3 pt-2">
|
|
<button
|
|
type="button"
|
|
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
|
@click="isFractionedDrawerOpen = false"
|
|
>
|
|
Annuler
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
|
>
|
|
Enregistrer
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</AppDrawer>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type {Absence} from '~/services/dto/absence'
|
|
import type {EmployeeLeaveSummary} from '~/services/dto/employee-leave-summary'
|
|
import {normalizeDate, toYmd} from '~/utils/date'
|
|
import AppDrawer from '~/components/AppDrawer.vue'
|
|
|
|
type DayLeaveState = {
|
|
am: boolean
|
|
pm: boolean
|
|
labels: string[]
|
|
colors: string[]
|
|
}
|
|
|
|
const props = defineProps<{
|
|
absences: Absence[]
|
|
summary: EmployeeLeaveSummary | null
|
|
publicHolidays: Record<string, string>
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(event: 'update-fractioned-days', days: number): void
|
|
}>()
|
|
|
|
const isFractionedDrawerOpen = ref(false)
|
|
const fractionedForm = reactive({ days: 0 })
|
|
|
|
const openFractionedDrawer = () => {
|
|
fractionedForm.days = props.summary?.fractionedDays ?? 0
|
|
isFractionedDrawerOpen.value = true
|
|
}
|
|
|
|
const handleSubmitFractioned = () => {
|
|
const value = Number(fractionedForm.days)
|
|
if (Number.isNaN(value) || value < 0) return
|
|
emit('update-fractioned-days', value)
|
|
isFractionedDrawerOpen.value = false
|
|
}
|
|
|
|
const monthLabels = [
|
|
'Janvier',
|
|
'Fevrier',
|
|
'Mars',
|
|
'Avril',
|
|
'Mai',
|
|
'Juin',
|
|
'Juillet',
|
|
'Aout',
|
|
'Septembre',
|
|
'Octobre',
|
|
'Novembre',
|
|
'Decembre'
|
|
] as const
|
|
|
|
const weekDayLabels = ['L', 'M', 'M', 'J', 'V', 'S', 'D'] as const
|
|
|
|
const isForfaitRule = computed(() => props.summary?.ruleCode === 'FORFAIT_218')
|
|
|
|
const displayedYear = computed(() => {
|
|
if (props.summary?.year) return props.summary.year
|
|
const today = new Date()
|
|
const year = today.getFullYear()
|
|
const month = today.getMonth() + 1
|
|
return month >= 6 ? year + 1 : year
|
|
})
|
|
|
|
const orderedMonthIndexes = computed(() => {
|
|
if (isForfaitRule.value) return [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
|
return [5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4]
|
|
})
|
|
|
|
const buildDateFromYmd = (value: string) => new Date(`${value}T00:00:00`)
|
|
|
|
const dayLeaveMap = computed(() => {
|
|
const map = new Map<string, DayLeaveState>()
|
|
|
|
for (const absence of props.absences) {
|
|
const startYmd = normalizeDate(absence.startDate)
|
|
const endYmd = normalizeDate(absence.endDate)
|
|
const start = buildDateFromYmd(startYmd)
|
|
const end = buildDateFromYmd(endYmd)
|
|
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) continue
|
|
|
|
for (const cursor = new Date(start); cursor <= end; cursor.setDate(cursor.getDate() + 1)) {
|
|
const ymd = toYmd(cursor.getFullYear(), cursor.getMonth(), cursor.getDate())
|
|
const existing = map.get(ymd) ?? {
|
|
am: false,
|
|
pm: false,
|
|
labels: [] as string[],
|
|
colors: [] as string[]
|
|
}
|
|
|
|
const isStart = ymd === startYmd
|
|
const isEnd = ymd === endYmd
|
|
const isSingleDay = startYmd === endYmd
|
|
|
|
let am = false
|
|
let pm = false
|
|
|
|
if (isSingleDay) {
|
|
am = absence.startHalf === 'AM'
|
|
pm = absence.endHalf === 'PM'
|
|
} else if (isStart) {
|
|
am = absence.startHalf === 'AM'
|
|
pm = true
|
|
} else if (isEnd) {
|
|
am = true
|
|
pm = absence.endHalf === 'PM'
|
|
} else {
|
|
am = true
|
|
pm = true
|
|
}
|
|
|
|
const typeLabel = absence.type?.label ?? absence.type?.code ?? 'Absence'
|
|
const typeColor = absence.type?.color ?? '#222783'
|
|
const halfSuffix = am && !pm ? ' (Matin)' : (!am && pm ? ' (Apres-midi)' : '')
|
|
const hoverLabel = `${typeLabel}${halfSuffix}`
|
|
|
|
const colors = existing.colors.includes(typeColor)
|
|
? existing.colors
|
|
: [...existing.colors, typeColor]
|
|
|
|
map.set(ymd, {
|
|
am: existing.am || am,
|
|
pm: existing.pm || pm,
|
|
labels: existing.labels.includes(hoverLabel)
|
|
? existing.labels
|
|
: [...existing.labels, hoverLabel],
|
|
colors
|
|
})
|
|
}
|
|
}
|
|
|
|
return map
|
|
})
|
|
|
|
const months = computed(() => {
|
|
return orderedMonthIndexes.value.map((monthIndex) => {
|
|
const label = monthLabels[monthIndex]
|
|
const monthYear = isForfaitRule.value
|
|
? displayedYear.value
|
|
: (monthIndex >= 5 ? displayedYear.value - 1 : displayedYear.value)
|
|
|
|
const first = new Date(monthYear, monthIndex, 1)
|
|
const daysInMonth = new Date(monthYear, monthIndex + 1, 0).getDate()
|
|
const mondayBasedFirstDay = (first.getDay() + 6) % 7
|
|
|
|
const cells: Array<{ ymd: string; label: string; leave: DayLeaveState | null; isHoliday: boolean } | null> = []
|
|
|
|
for (let i = 0; i < mondayBasedFirstDay; i += 1) {
|
|
cells.push(null)
|
|
}
|
|
|
|
for (let day = 1; day <= daysInMonth; day += 1) {
|
|
const ymd = toYmd(monthYear, monthIndex, day)
|
|
cells.push({
|
|
ymd,
|
|
label: String(day),
|
|
leave: dayLeaveMap.value.get(ymd) ?? null,
|
|
isHoliday: ymd in props.publicHolidays
|
|
})
|
|
}
|
|
|
|
while (cells.length % 7 !== 0) {
|
|
cells.push(null)
|
|
}
|
|
|
|
return {
|
|
label,
|
|
cells
|
|
}
|
|
})
|
|
})
|
|
|
|
const getDayClass = (day: { leave: DayLeaveState | null; isHoliday: boolean }) => {
|
|
if (day.leave) {
|
|
return 'rounded font-semibold text-white'
|
|
}
|
|
if (day.isHoliday) return 'text-primary-500 rounded font-semibold'
|
|
return 'text-primary-500'
|
|
}
|
|
|
|
const getDayStyle = (day: { leave: DayLeaveState | null; isHoliday: boolean }) => {
|
|
if (day.leave) {
|
|
const color = day.leave.colors[0] ?? '#222783'
|
|
if (day.leave.am && day.leave.pm) {
|
|
return { backgroundColor: color }
|
|
}
|
|
const colorFaded = `${color}60`
|
|
const backgroundImage = day.leave.am
|
|
? `linear-gradient(180deg, ${color} 0 50%, ${colorFaded} 50% 100%)`
|
|
: `linear-gradient(180deg, ${colorFaded} 0 50%, ${color} 50% 100%)`
|
|
return { backgroundImage, backgroundColor: 'transparent' }
|
|
}
|
|
if (day.isHoliday) return { backgroundColor: 'rgb(179, 229, 252)' }
|
|
return undefined
|
|
}
|
|
|
|
const getDayText = (day: { label: string; leave: DayLeaveState | null }) => {
|
|
return day.label
|
|
}
|
|
|
|
const getDayTitle = (day: { leave: DayLeaveState | null; isHoliday: boolean; ymd: string }) => {
|
|
if (day.leave && day.leave.labels.length > 0) return day.leave.labels.join(' / ')
|
|
if (day.isHoliday) return props.publicHolidays[day.ymd] ?? 'Jour férié'
|
|
return ''
|
|
}
|
|
|
|
const formatCount = (value: number | null | undefined) => {
|
|
if (value === null || value === undefined) return '-'
|
|
const rounded = Math.round(value * 100) / 100
|
|
if (Number.isInteger(rounded)) return String(rounded)
|
|
return rounded.toFixed(2).replace('.', ',')
|
|
}
|
|
</script>
|