diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 7db35a3..502e17c 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -5,7 +5,8 @@
"Bash(npx nuxi:*)",
"Bash(php:*)",
"Bash(docker compose:*)",
- "Bash(make test:*)"
+ "Bash(make test:*)",
+ "Bash(grep:*)"
]
}
}
diff --git a/doc/functional-rules.md b/doc/functional-rules.md
index 3e31acc..ac5aae9 100644
--- a/doc/functional-rules.md
+++ b/doc/functional-rules.md
@@ -43,10 +43,17 @@ Documents complementaires:
- Saisie par salarié et par date:
- matin / après-midi / soir
- pour `PRESENCE`: demi-journées matin/après-midi
+- Sélecteur de temps:
+ - créneaux de 15 minutes uniquement (00:00, 00:15, ..., 23:45)
+ - saisie libre possible mais valeur vidée au blur si hors options
- Calculs affichés:
- `Jour`, `Nuit`, `Total`
- Heures de nuit:
- fenêtres `00:00-06:00` et `21:00-24:00`
+- Date de modification (`updatedAt`):
+ - mise à jour uniquement quand un employé (`ROLE_SELF`) modifie ses propres heures
+ - non mise à jour lors de modifications admin ou chef de site
+ - affichée sous le nom de l'employé (visible admin uniquement)
## 4) Absences
@@ -57,8 +64,9 @@ Documents complementaires:
- Colonne absence (vue jour):
- affiche le libellé
- fond coloré selon le type d'absence
-- Si plusieurs absences de couleurs différentes sur le même jour:
- - fallback rouge
+- Calendrier congés: fond coloré selon la couleur du type d'absence (`AbsenceType.color`)
+ - demi-journée: dégradé diagonal
+ - journée complète: fond plein
### Effet absence sur les heures
@@ -73,7 +81,7 @@ Documents complementaires:
- Si `countAsWorkedHours = true`:
- `TIME`: crédit de minutes selon contrat actif du jour
- - `PRESENCE`: crédit d'unités (0.5 / demi-journée)
+ - `PRESENCE` (forfait): aucun crédit de présence (seules les checkboxes cochées comptent)
## 5) Validations des lignes d'heures
@@ -112,6 +120,7 @@ Documents complementaires:
## 7) Fériés
- Les jours fériés sont identifiés et affichés
+- Onglet congés: jours fériés affichés sur le calendrier avec fond `rgb(179, 229, 252)` et nom au survol
- Règle courante:
- absences bloquées sur jour férié
- saisie d'heures autorisée
diff --git a/frontend/components/employees/ContractTab.vue b/frontend/components/employees/ContractTab.vue
index b464f26..a8255e8 100644
--- a/frontend/components/employees/ContractTab.vue
+++ b/frontend/components/employees/ContractTab.vue
@@ -86,6 +86,19 @@
La date de fin est obligatoire.
+
{ dayMinutes: number; nightMinutes: number; totalMinutes: number }
getRowAbsenceLabel: (employeeId: number) => string
getRowAbsenceStyle: (employeeId: number) => { backgroundColor: string } | undefined
+ getRowUpdatedAt: (employeeId: number) => string
getPresenceDayValue: (employeeId: number) => string
onAbsenceClick: (employeeId: number) => void
formatMinutes: (minutes: number) => string
diff --git a/frontend/components/hours/types.ts b/frontend/components/hours/types.ts
index 086413c..d273f88 100644
--- a/frontend/components/hours/types.ts
+++ b/frontend/components/hours/types.ts
@@ -10,4 +10,5 @@ export type HourRow = {
isPresentAfternoon: boolean
isSiteValid: boolean
isValid: boolean
+ updatedAt: string | null
}
diff --git a/frontend/components/ui/TimeSelect.vue b/frontend/components/ui/TimeSelect.vue
index 6ffec40..ef1db23 100644
--- a/frontend/components/ui/TimeSelect.vue
+++ b/frontend/components/ui/TimeSelect.vue
@@ -167,8 +167,9 @@ const closeMenu = () => {
const commitInput = () => {
const normalized = normalizeTypedTime(inputValue.value)
- if (normalized === null) {
- inputValue.value = props.modelValue
+ if (normalized === null || (normalized !== '' && !timeSlots.value.includes(normalized))) {
+ emit('update:modelValue', '')
+ inputValue.value = ''
closeMenu()
return
}
diff --git a/frontend/composables/useEmployeeDetailPage.ts b/frontend/composables/useEmployeeDetailPage.ts
index 4f6e3f6..e44a56a 100644
--- a/frontend/composables/useEmployeeDetailPage.ts
+++ b/frontend/composables/useEmployeeDetailPage.ts
@@ -9,6 +9,7 @@ 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 { listPublicHolidays } from '~/services/public-holidays'
import { formatNullableYmdToFr, getTodayYmd, shiftYmd } from '~/utils/date'
import { contractNatureLabel, isContractNature, requiresContractEndDate } from '~/utils/contract'
@@ -22,6 +23,7 @@ export const useEmployeeDetailPage = () => {
const employeeAbsences = ref([])
const leaveSummary = ref(null)
const rttSummary = ref(null)
+ const publicHolidays = ref>({})
const isContractDrawerOpen = ref(false)
const isContractSubmitting = ref(false)
const isCreateContractDrawerOpen = ref(false)
@@ -34,7 +36,8 @@ export const useEmployeeDetailPage = () => {
contractNature: 'CDI' as 'CDI' | 'CDD' | 'INTERIM',
startDate: '',
endDate: '',
- paidLeaveSettled: false
+ paidLeaveSettled: false,
+ comment: ''
})
const validationTouched = reactive({
@@ -138,6 +141,7 @@ export const useEmployeeDetailPage = () => {
contractForm.startDate = active.startDate
contractForm.endDate = getTodayYmd()
contractForm.paidLeaveSettled = false
+ contractForm.comment = ''
}
const openCloseContractDrawer = () => {
@@ -198,7 +202,10 @@ export const useEmployeeDetailPage = () => {
const to = isForfait
? `${leaveYear}-12-31`
: `${leaveYear}-05-31`
- const [absences, summary, rtt] = await Promise.all([
+ const holidayYears = isForfait
+ ? [leaveYear]
+ : [leaveYear - 1, leaveYear]
+ const [absences, summary, rtt, ...holidayResults] = await Promise.all([
listAbsences({
from,
to,
@@ -207,11 +214,13 @@ export const useEmployeeDetailPage = () => {
showLeaveTab.value
? getEmployeeLeaveSummary(loadedEmployee.id, leaveYear)
: Promise.resolve(null),
- getEmployeeRttSummary(loadedEmployee.id, rttYear)
+ getEmployeeRttSummary(loadedEmployee.id, rttYear),
+ ...holidayYears.map((y) => listPublicHolidays('metropole', y))
])
employeeAbsences.value = absences
leaveSummary.value = summary
rttSummary.value = rtt
+ publicHolidays.value = Object.assign({}, ...holidayResults)
if (!showLeaveTab.value && activeTab.value === 'leave') {
activeTab.value = 'contract'
}
@@ -242,7 +251,8 @@ export const useEmployeeDetailPage = () => {
siteId: employee.value.site?.id ?? null,
contractId: Number(contractForm.contractId),
contractEndDate: contractForm.endDate || null,
- contractPaidLeaveSettled: contractForm.paidLeaveSettled
+ contractPaidLeaveSettled: contractForm.paidLeaveSettled,
+ contractComment: contractForm.comment || null
})
isContractDrawerOpen.value = false
@@ -309,6 +319,7 @@ export const useEmployeeDetailPage = () => {
employeeAbsences,
leaveSummary,
rttSummary,
+ publicHolidays,
showLeaveTab,
contractHistory,
employeeContractWorkLabel,
diff --git a/frontend/composables/useHoursPage.ts b/frontend/composables/useHoursPage.ts
index 864291a..e5f2e47 100644
--- a/frontend/composables/useHoursPage.ts
+++ b/frontend/composables/useHoursPage.ts
@@ -341,7 +341,8 @@ export const useHoursPage = () => {
isPresentMorning: false,
isPresentAfternoon: false,
isSiteValid: false,
- isValid: false
+ isValid: false,
+ updatedAt: null
})
const isPresenceTracking = (employee: Employee) => employee.contract?.trackingMode === TRACKING_MODES.PRESENCE
@@ -463,6 +464,14 @@ export const useHoursPage = () => {
return { backgroundColor: dayRow.absenceColor || '#dc2626' }
}
+ const getRowUpdatedAt = (employeeId: number): string => {
+ const raw = rows.value[employeeId]?.updatedAt
+ if (!raw) return ''
+ const date = new Date(raw)
+ if (Number.isNaN(date.getTime())) return ''
+ return date.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })
+ }
+
const getPresenceDayValue = (employeeId: number) => {
const row = rows.value[employeeId]
const dayRow = dayContextByEmployeeId.value.get(employeeId)
@@ -521,7 +530,8 @@ export const useHoursPage = () => {
isPresentMorning: workHour?.isPresentMorning ?? false,
isPresentAfternoon: workHour?.isPresentAfternoon ?? false,
isSiteValid: workHour?.isSiteValid ?? false,
- isValid: workHour?.isValid ?? false
+ isValid: workHour?.isValid ?? false,
+ updatedAt: workHour?.updatedAt ?? null
}
}
@@ -1140,6 +1150,7 @@ export const useHoursPage = () => {
getRowMetrics,
getRowAbsenceLabel,
getRowAbsenceStyle,
+ getRowUpdatedAt,
getPresenceDayValue,
openAbsenceDrawer,
submitAbsence,
diff --git a/frontend/pages/employees/[id].vue b/frontend/pages/employees/[id].vue
index 8dc8407..16d7389 100644
--- a/frontend/pages/employees/[id].vue
+++ b/frontend/pages/employees/[id].vue
@@ -93,6 +93,7 @@
class="h-full"
:absences="employeeAbsences"
:summary="leaveSummary"
+ :public-holidays="publicHolidays"
/>
@@ -109,6 +110,7 @@ const {
employeeAbsences,
leaveSummary,
rttSummary,
+ publicHolidays,
showLeaveTab,
contractHistory,
employeeContractWorkLabel,
diff --git a/frontend/pages/hours.vue b/frontend/pages/hours.vue
index 9245760..a37d02a 100644
--- a/frontend/pages/hours.vue
+++ b/frontend/pages/hours.vue
@@ -66,6 +66,7 @@
:get-row-metrics="getRowMetrics"
:get-row-absence-label="getRowAbsenceLabel"
:get-row-absence-style="getRowAbsenceStyle"
+ :get-row-updated-at="getRowUpdatedAt"
:get-presence-day-value="getPresenceDayValue"
:on-absence-click="openAbsenceDrawer"
:format-minutes="formatMinutes"
@@ -173,6 +174,7 @@ const {
getRowMetrics,
getRowAbsenceLabel,
getRowAbsenceStyle,
+ getRowUpdatedAt,
getPresenceDayValue,
openAbsenceDrawer,
submitAbsence,
diff --git a/frontend/services/dto/employee.ts b/frontend/services/dto/employee.ts
index d1b1056..ceaac93 100644
--- a/frontend/services/dto/employee.ts
+++ b/frontend/services/dto/employee.ts
@@ -8,6 +8,7 @@ export type ContractHistoryItem = {
contractNature: 'CDI' | 'CDD' | 'INTERIM'
startDate: string
endDate?: string | null
+ comment?: string | null
}
export type Employee = {
diff --git a/frontend/services/dto/work-hour.ts b/frontend/services/dto/work-hour.ts
index 7f4dbac..953eada 100644
--- a/frontend/services/dto/work-hour.ts
+++ b/frontend/services/dto/work-hour.ts
@@ -15,6 +15,7 @@ export type WorkHour = {
isPresentAfternoon?: boolean
isSiteValid?: boolean
isValid?: boolean
+ updatedAt?: string | null
}
export type WorkHourEntryPayload = {
diff --git a/frontend/services/employees.ts b/frontend/services/employees.ts
index 00d470c..19dd6e6 100644
--- a/frontend/services/employees.ts
+++ b/frontend/services/employees.ts
@@ -61,6 +61,7 @@ export const updateEmployee = async (
contractStartDate?: string
contractEndDate?: string | null
contractPaidLeaveSettled?: boolean
+ contractComment?: string | null
displayOrder?: number
}
) => {
@@ -87,6 +88,9 @@ export const updateEmployee = async (
if (payload.contractPaidLeaveSettled !== undefined) {
body.contractPaidLeaveSettled = payload.contractPaidLeaveSettled
}
+ if (payload.contractComment !== undefined) {
+ body.contractComment = payload.contractComment ?? null
+ }
return api.patch