feat : ajout de la gestion RTT

This commit is contained in:
2026-03-06 15:00:55 +01:00
parent 20a651895f
commit 4cf2608cdd
18 changed files with 1327 additions and 99 deletions

View File

@@ -1,56 +1,65 @@
<template>
<section class="mt-8">
<div class="grid grid-cols-4 rounded-md bg-primary-500 text-white text-[20]">
<div class="flex flex-col jutify-center items-center border-r-4 border-white py-3">
<p><strong class="uppercase font-semibold">Acquis année :</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 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 jutify-center items-center border-r-4 border-white py-3">
<p><span class="uppercase font-semibold">Acquis fractionné :</span></p>
<p>{{ formatCount(summary?.fractionedDays) }} Jours</p>
</div>
<div class="flex flex-col jutify-center 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 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>
<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 jutify-center items-center border-r-4 border-white py-3">
<p><strong class="uppercase font-semibold">Acquis année :</strong> {{
formatCount(summary?.acquiredDays)
}} Jours</p>
<p><strong class="uppercase font-semibold">Reste à prendre :</strong>
{{ formatCount(summary?.remainingDays) }} Jours</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.leave)"
:style="getDayStyle(day.leave)"
:title="getDayTitle(day.leave)"
>
{{ getDayText(day) }}
<div class="flex flex-col 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 jutify-center items-center border-r-4 border-white py-3">
<p><span class="uppercase font-semibold">Acquis fractionné :</span></p>
<p>{{ formatCount(summary?.fractionedDays) }} Jours</p>
</div>
<div class="flex flex-col jutify-center 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.leave)"
:style="getDayStyle(day.leave)"
:title="getDayTitle(day.leave)"
>
{{ getDayText(day) }}
</div>
</div>
</template>
</div>
</div>
</template>
</div>
</div>
</div>
</section>
</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 type {Absence} from '~/services/dto/absence'
import type {EmployeeLeaveSummary} from '~/services/dto/employee-leave-summary'
import {normalizeDate, toYmd} from '~/utils/date'
type DayLeaveState = {
am: boolean
@@ -212,12 +221,12 @@ const getDayStyle = (leave: DayLeaveState | null) => {
const color = leave.hasOtherTypes ? '#dc2626' : '#222783'
const backgroundImage = leave.am
? `linear-gradient(135deg, ${color} 0 50%, transparent 50% 100%)`
: `linear-gradient(135deg, transparent 0 50%, ${color} 50% 100%)`
? `linear-gradient(135deg, ${color} 0 50%, transparent 50% 100%)`
: `linear-gradient(135deg, transparent 0 50%, ${color} 50% 100%)`
return {
backgroundImage,
backgroundColor: 'transparent'
backgroundImage,
backgroundColor: 'transparent'
}
}

View File

@@ -1,7 +1,122 @@
<template>
<section class="mt-8">
<div class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
Bloc RTT (à implémenter)
<section class="flex h-full min-h-0 flex-col overflow-hidden pt-8">
<div class="flex justify-center items-center bg-primary-500 rounded-md text-white py-5 text-[20px]">
<p><span class="font-semibold uppercase">RTT à la date du jour :</span> {{ formatDays(summary?.availableMinutes ?? 0) }}</p>
</div>
<div class="mt-8 min-h-0 flex-1 overflow-y-auto pr-2">
<div class="grid grid-cols-4 gap-10 pb-4">
<div
v-for="month in months"
:key="month.month"
class="rounded-md bg-tertiary-500 text-primary-500"
>
<div class="flex justify-center rounded-t-md bg-primary-500 py-3 font-bold text-white text-[18px]">
{{ month.label }}
</div>
<div class="grid grid-cols-[60%_40%] text-[18px] border border-primary-500">
<template v-for="week in month.weeks" :key="week.key">
<div class="py-[6px] pl-3 border-r border-b border-primary-500">
<span v-if="week.isEmpty">&nbsp;</span>
<span v-else>Semaine {{ week.weekNumber }}</span>
</div>
<div class="py-[6px] pl-3 border-b border-primary-500">
<span v-if="week.isEmpty">&nbsp;</span>
<span v-else>{{ formatMinutes(week.recoveryMinutes) }}</span>
</div>
</template>
<div class="py-[6px] pl-3 border-r border-b border-primary-500 font-semibold">Total</div>
<div class="py-[6px] pl-3 border-b border-primary-500 font-semibold">{{ formatMinutes(month.totalMinutes) }}</div>
<div class="py-[6px] pl-3 border-r border-primary-500">Heure payée</div>
<div class="py-[6px] pl-3">0h</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import type { EmployeeRttSummary } from '~/services/dto/employee-rtt-summary'
const props = defineProps<{
summary: EmployeeRttSummary | null
}>()
const monthLabels = [
'Janvier',
'Fevrier',
'Mars',
'Avril',
'Mai',
'Juin',
'Juillet',
'Aout',
'Septembre',
'Octobre',
'Novembre',
'Decembre'
] as const
const months = computed(() => {
type DisplayWeek = {
key: string
weekNumber: number
recoveryMinutes: number
isEmpty?: boolean
}
const byMonth = new Map<number, { month: number; label: string; weeks: DisplayWeek[]; totalMinutes: number }>()
const orderedMonths = [6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5]
for (const month of orderedMonths) {
byMonth.set(month, {
month,
label: monthLabels[month - 1],
weeks: [],
totalMinutes: 0
})
}
for (const week of props.summary?.weeks ?? []) {
const month = byMonth.get(week.month)
if (!month) continue
month.weeks.push({
key: week.weekStart,
weekNumber: week.weekNumber,
recoveryMinutes: week.recoveryMinutes
})
month.totalMinutes += week.recoveryMinutes
}
return orderedMonths
.map((monthNumber) => byMonth.get(monthNumber)!)
.filter(Boolean)
.map((month) => {
const minRows = 5
const missing = Math.max(0, minRows - month.weeks.length)
for (let i = 0; i < missing; i += 1) {
month.weeks.push({
key: `empty-${month.month}-${i}`,
weekNumber: 0,
recoveryMinutes: 0,
isEmpty: true
})
}
return month
})
})
const formatMinutes = (minutes: number) => {
const abs = Math.abs(minutes)
const hours = Math.floor(abs / 60)
const rest = abs % 60
const sign = minutes < 0 ? '-' : ''
return `${sign}${hours}h${rest.toString().padStart(2, '0')}`
}
const formatDays = (minutes: number) => {
const days = minutes / 420
const sign = days < 0 ? '-' : ''
return `${sign}${Math.abs(days).toFixed(2)} j`
}
</script>

View File

@@ -1,11 +1,13 @@
import type { Contract } from '~/services/dto/contract'
import type { Absence } from '~/services/dto/absence'
import type { EmployeeLeaveSummary } from '~/services/dto/employee-leave-summary'
import type { EmployeeRttSummary } from '~/services/dto/employee-rtt-summary'
import type { ContractHistoryItem, Employee } from '~/services/dto/employee'
import { CONTRACT_TYPES } from '~/services/dto/contract'
import { listAbsences } from '~/services/absences'
import { listContracts } from '~/services/contracts'
import { getEmployeeLeaveSummary } from '~/services/employee-leave-summary'
import { getEmployeeRttSummary } from '~/services/employee-rtt-summary'
import { getEmployee, updateEmployee } from '~/services/employees'
import { formatNullableYmdToFr, getTodayYmd, shiftYmd } from '~/utils/date'
import { contractNatureLabel, isContractNature, requiresContractEndDate } from '~/utils/contract'
@@ -19,6 +21,7 @@ export const useEmployeeDetailPage = () => {
const contracts = ref<Contract[]>([])
const employeeAbsences = ref<Absence[]>([])
const leaveSummary = ref<EmployeeLeaveSummary | null>(null)
const rttSummary = ref<EmployeeRttSummary | null>(null)
const isContractDrawerOpen = ref(false)
const isContractSubmitting = ref(false)
const isCreateContractDrawerOpen = ref(false)
@@ -188,13 +191,14 @@ export const useEmployeeDetailPage = () => {
const leaveYear = isForfait
? now.getFullYear()
: (now.getMonth() >= 5 ? now.getFullYear() + 1 : now.getFullYear())
const rttYear = now.getMonth() >= 5 ? now.getFullYear() + 1 : now.getFullYear()
const from = isForfait
? `${leaveYear}-01-01`
: `${leaveYear - 1}-06-01`
const to = isForfait
? `${leaveYear}-12-31`
: `${leaveYear}-05-31`
const [absences, summary] = await Promise.all([
const [absences, summary, rtt] = await Promise.all([
listAbsences({
from,
to,
@@ -202,10 +206,12 @@ export const useEmployeeDetailPage = () => {
}),
showLeaveTab.value
? getEmployeeLeaveSummary(loadedEmployee.id, leaveYear)
: Promise.resolve(null)
: Promise.resolve(null),
getEmployeeRttSummary(loadedEmployee.id, rttYear)
])
employeeAbsences.value = absences
leaveSummary.value = summary
rttSummary.value = rtt
if (!showLeaveTab.value && activeTab.value === 'leave') {
activeTab.value = 'contract'
}
@@ -302,6 +308,7 @@ export const useEmployeeDetailPage = () => {
contracts,
employeeAbsences,
leaveSummary,
rttSummary,
showLeaveTab,
contractHistory,
employeeContractWorkLabel,

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div class="h-full overflow-hidden flex flex-col">
<div v-if="isLoading" class="mt-6 rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
Chargement...
@@ -10,7 +10,7 @@
Employé introuvable.
</div>
<div v-else>
<div v-else class="flex min-h-0 flex-1 flex-col">
<div class="flex items-center justify-between">
<h1 class="text-4xl font-bold text-primary-500">{{ employee.firstName }} {{ employee.lastName }}</h1>
<div class="text-right">
@@ -53,42 +53,49 @@
</button>
</div>
</div>
<EmployeesContractTab
v-if="activeTab === 'contract'"
:contract-history="contractHistory"
:contract-nature-label="contractNatureLabel"
:contract-history-label="contractHistoryLabel"
:format-date="formatDate"
:is-contract-submitting="isContractSubmitting"
:can-close-current-contract="canCloseCurrentContract"
:is-create-contract-submitting="isCreateContractSubmitting"
:contracts="contracts"
:can-create-contract="canCreateContract"
:is-contract-drawer-open="isContractDrawerOpen"
:contract-form="contractForm"
:readonly-field-class="readonlyFieldClass"
:close-contract-worked-hours-label="closeContractWorkedHoursLabel"
:contract-end-date-field-class="contractEndDateFieldClass"
:show-contract-end-date-error="showContractEndDateError"
:is-contract-end-date-valid="isContractEndDateValid"
:is-create-contract-drawer-open="isCreateContractDrawerOpen"
:create-contract-form="createContractForm"
:create-contract-nature-field-class="createContractNatureFieldClass"
:create-contract-field-class="createContractFieldClass"
:create-contract-start-date-field-class="createContractStartDateFieldClass"
:requires-create-contract-end-date="requiresCreateContractEndDate"
:create-contract-end-date-field-class="createContractEndDateFieldClass"
:is-create-contract-form-valid="isCreateContractFormValid"
:on-open-close-contract-drawer="openCloseContractDrawer"
:on-open-create-contract-drawer="openCreateContractDrawer"
:on-update-contract-drawer-open="setContractDrawerOpen"
:on-update-create-contract-drawer-open="setCreateContractDrawerOpen"
:on-submit-close-contract="submitContractUpdate"
:on-submit-create-contract="submitCreateContract"
/>
<EmployeesLeaveTab v-else-if="showLeaveTab && activeTab === 'leave'" :absences="employeeAbsences" :summary="leaveSummary" />
<EmployeesRttTab v-else />
<div class="min-h-0 flex-1">
<EmployeesContractTab
v-if="activeTab === 'contract'"
class="h-full overflow-y-auto pr-1"
:contract-history="contractHistory"
:contract-nature-label="contractNatureLabel"
:contract-history-label="contractHistoryLabel"
:format-date="formatDate"
:is-contract-submitting="isContractSubmitting"
:can-close-current-contract="canCloseCurrentContract"
:is-create-contract-submitting="isCreateContractSubmitting"
:contracts="contracts"
:can-create-contract="canCreateContract"
:is-contract-drawer-open="isContractDrawerOpen"
:contract-form="contractForm"
:readonly-field-class="readonlyFieldClass"
:close-contract-worked-hours-label="closeContractWorkedHoursLabel"
:contract-end-date-field-class="contractEndDateFieldClass"
:show-contract-end-date-error="showContractEndDateError"
:is-contract-end-date-valid="isContractEndDateValid"
:is-create-contract-drawer-open="isCreateContractDrawerOpen"
:create-contract-form="createContractForm"
:create-contract-nature-field-class="createContractNatureFieldClass"
:create-contract-field-class="createContractFieldClass"
:create-contract-start-date-field-class="createContractStartDateFieldClass"
:requires-create-contract-end-date="requiresCreateContractEndDate"
:create-contract-end-date-field-class="createContractEndDateFieldClass"
:is-create-contract-form-valid="isCreateContractFormValid"
:on-open-close-contract-drawer="openCloseContractDrawer"
:on-open-create-contract-drawer="openCreateContractDrawer"
:on-update-contract-drawer-open="setContractDrawerOpen"
:on-update-create-contract-drawer-open="setCreateContractDrawerOpen"
:on-submit-close-contract="submitContractUpdate"
:on-submit-create-contract="submitCreateContract"
/>
<EmployeesLeaveTab
v-else-if="showLeaveTab && activeTab === 'leave'"
class="h-full"
:absences="employeeAbsences"
:summary="leaveSummary"
/>
<EmployeesRttTab v-else class="h-full" :summary="rttSummary" />
</div>
</div>
</div>
</template>
@@ -101,6 +108,7 @@ const {
contracts,
employeeAbsences,
leaveSummary,
rttSummary,
showLeaveTab,
contractHistory,
employeeContractWorkLabel,

View File

@@ -0,0 +1,16 @@
export type EmployeeRttWeekSummary = {
month: number
weekNumber: number
weekStart: string
weekEnd: string
recoveryMinutes: number
}
export type EmployeeRttSummary = {
year: number
carryFromPreviousYearMinutes: number
currentYearRecoveryMinutes: number
availableMinutes: number
weeks: EmployeeRttWeekSummary[]
}

View File

@@ -0,0 +1,8 @@
import type { EmployeeRttSummary } from './dto/employee-rtt-summary'
export const getEmployeeRttSummary = async (employeeId: number, year?: number) => {
const api = useApi()
const query = year ? { year } : {}
return api.get<EmployeeRttSummary>(`/employees/${employeeId}/rtt-summary`, query, { toast: false })
}