feat(absence) : extract Absence front into Nuxt module layer

LST-66 (2.3) front. Companion to the backend module migration.

- Move pages (absences, team-absences), 8 components, the absences service +
  DTO and the useAbsenceHelpers composable into frontend/modules/absence/
  (auto-detected layer; composable now auto-imported).
- Rewrite consumers: AdminAbsencePolicyTab and the time-tracking calendar
  (getPublicHolidays) point to ~/modules/absence/...
- Middlewares (employee/admin) and shared services (clients, users,
  user-data DTO) stay at the root. i18n stays global.
- Routes /absences and /team-absences preserved.

nuxt build passes; routes confirmed.
This commit is contained in:
Matthieu
2026-06-20 18:36:48 +02:00
parent 306cfd34cd
commit 163bf0891a
16 changed files with 21 additions and 27 deletions
-137
View File
@@ -1,137 +0,0 @@
import type {
AbsenceBalance,
AbsencePolicy,
AbsencePolicyWrite,
AbsencePreviewPayload,
AbsencePreviewResult,
AbsenceRequest,
AbsenceRequestWrite,
AbsenceStatus,
AbsenceType,
} from './dto/absence'
import type { HydraCollection } from '~/utils/api'
import { extractHydraMembers } from '~/utils/api'
export type AbsenceRequestFilters = {
status?: AbsenceStatus
type?: AbsenceType
year?: number
user?: number
}
export function useAbsenceService() {
const api = useApi()
// --- Requests ---
async function getRequests(filters: AbsenceRequestFilters = {}): Promise<AbsenceRequest[]> {
const query: Record<string, unknown> = {}
if (filters.status) query.status = filters.status
if (filters.type) query.type = filters.type
if (filters.year) query.year = filters.year
if (filters.user) query.user = `/api/users/${filters.user}`
const data = await api.get<HydraCollection<AbsenceRequest>>('/absence_requests', query)
return extractHydraMembers(data)
}
async function getRequest(id: number): Promise<AbsenceRequest> {
return api.get<AbsenceRequest>(`/absence_requests/${id}`)
}
async function create(payload: AbsenceRequestWrite): Promise<AbsenceRequest> {
return api.post<AbsenceRequest>('/absence_requests', payload as Record<string, unknown>, {
toastSuccessKey: 'absences.toast.created',
})
}
async function preview(payload: AbsencePreviewPayload): Promise<AbsencePreviewResult> {
return api.post<AbsencePreviewResult>('/absence_requests/preview', payload as Record<string, unknown>, {
toast: false,
})
}
async function approve(id: number): Promise<AbsenceRequest> {
return api.patch<AbsenceRequest>(`/absence_requests/${id}/approve`, {}, {
toastSuccessKey: 'absences.toast.approved',
})
}
async function reject(id: number, rejectionReason: string): Promise<AbsenceRequest> {
return api.patch<AbsenceRequest>(`/absence_requests/${id}/reject`, { rejectionReason }, {
toastSuccessKey: 'absences.toast.rejected',
})
}
async function cancel(id: number): Promise<AbsenceRequest> {
return api.patch<AbsenceRequest>(`/absence_requests/${id}/cancel`, {}, {
toastSuccessKey: 'absences.toast.cancelled',
})
}
async function uploadJustification(id: number, file: File): Promise<AbsenceRequest> {
const form = new FormData()
form.append('file', file)
return api.post<AbsenceRequest>(`/absence_requests/${id}/justificatif`, form as unknown as Record<string, unknown>, {
toastSuccessKey: 'absences.toast.justificationUploaded',
})
}
// --- Balances ---
async function getBalances(filters: { user?: number; period?: string; type?: AbsenceType } = {}): Promise<AbsenceBalance[]> {
const query: Record<string, unknown> = {}
if (filters.user) query.user = `/api/users/${filters.user}`
if (filters.period) query.period = filters.period
if (filters.type) query.type = filters.type
const data = await api.get<HydraCollection<AbsenceBalance>>('/absence_balances', query)
return extractHydraMembers(data)
}
async function adjustBalance(id: number, payload: { acquired?: number; acquiring?: number; taken?: number }): Promise<AbsenceBalance> {
return api.patch<AbsenceBalance>(`/absence_balances/${id}`, payload as Record<string, unknown>, {
toastSuccessKey: 'absences.toast.balanceAdjusted',
})
}
// --- Policies ---
async function getPolicies(): Promise<AbsencePolicy[]> {
const data = await api.get<HydraCollection<AbsencePolicy>>('/absence_policies')
return extractHydraMembers(data)
}
async function updatePolicy(id: number, payload: AbsencePolicyWrite): Promise<AbsencePolicy> {
return api.patch<AbsencePolicy>(`/absence_policies/${id}`, payload as Record<string, unknown>, {
toastSuccessKey: 'absences.toast.policyUpdated',
})
}
// --- Admin calendar ---
async function getCalendar(from: string, to: string): Promise<AbsenceRequest[]> {
return api.get<AbsenceRequest[]>('/admin/absences/calendar', { from, to })
}
// --- Public holidays (computed server-side) ---
async function getPublicHolidays(from: string, to: string): Promise<Record<string, string>> {
return api.get<Record<string, string>>('/public_holidays', { from, to }, { toast: false })
}
return {
getRequests,
getRequest,
create,
preview,
approve,
reject,
cancel,
uploadJustification,
getBalances,
adjustBalance,
getPolicies,
updatePolicy,
getCalendar,
getPublicHolidays,
}
}
-93
View File
@@ -1,93 +0,0 @@
export type AbsenceType = 'cp' | 'mariage_pacs' | 'naissance' | 'conge_parental' | 'deces' | 'maladie'
export type AbsenceStatus = 'pending' | 'approved' | 'rejected' | 'cancelled'
export type HalfDay = 'matin' | 'apres_midi'
export type AbsenceUserRef = {
'@id'?: string
id: number
username: string
avatarUrl: string | null
}
export type AbsenceRequest = {
'@id'?: string
id: number
user: AbsenceUserRef
type: AbsenceType
label: string
startDate: string
endDate: string
startHalfDay: HalfDay | null
endHalfDay: HalfDay | null
countedDays: number
reason: string | null
justificationFileName: string | null
justificationUrl: string | null
status: AbsenceStatus
rejectionReason: string | null
createdAt: string
reviewedAt: string | null
reviewedBy: AbsenceUserRef | null
}
export type AbsenceRequestWrite = {
type: AbsenceType
startDate: string
endDate: string
startHalfDay?: HalfDay | null
endHalfDay?: HalfDay | null
reason?: string | null
}
export type AbsenceBalance = {
'@id'?: string
id: number
user: AbsenceUserRef
type: AbsenceType
label: string
period: string
acquired: number
acquiring: number
acquiredTotal: number
taken: number
pending: number
available: number
}
export type AbsencePolicy = {
'@id'?: string
id: number
type: AbsenceType
label: string
daysPerYear: number | null
daysPerEvent: number | null
justificationRequired: boolean
noticeDays: number
countWorkingDaysOnly: boolean
active: boolean
}
export type AbsencePolicyWrite = {
daysPerYear?: number | null
daysPerEvent?: number | null
justificationRequired?: boolean
noticeDays?: number
countWorkingDaysOnly?: boolean
active?: boolean
}
export type AbsencePreviewPayload = {
type: AbsenceType
startDate: string
endDate: string
startHalfDay?: HalfDay | null
endHalfDay?: HalfDay | null
}
export type AbsencePreviewResult = {
countedDays: number
period: string | null
available: number | null
projectedAvailable: number | null
justificationRequired: boolean
}