feat(absences) : avancement module absences + suppression du portail client
Deux lots regroupés sur la branche feat/absence-management. Suppression complète du portail client : - retire ROLE_CLIENT (security.yaml) ; User::getRoles() ajoute toujours ROLE_USER - supprime l'entité ClientTicket (+ repo, states, relations), User.client et User.allowedProjects, NotificationService, ProjectAllowedExtension, le bloc ROLE_CLIENT de MailAccessChecker - front : pages /portal, layout portal, composants client-ticket/, AdminClientTicketTab, services/dto/i18n/docs associés - fixtures : retire les users client-liot / client-acme - migration Version20260522110000 (drop client_ticket, user_allowed_projects, colonnes liées ; task_document.task_id -> NOT NULL) - tests : retire les cas obsolètes testant le blocage des clients sur le mail Module gestion des absences (WIP) : - entités / migrations (Version20260521160000, Version20260522090000) - pages absences.vue / team-absences.vue, composants frontend/components/absence/ - services front, AccrueLeaveCommand, PublicHolidayController Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
137
frontend/services/absences.ts
Normal file
137
frontend/services/absences.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { ClientTicket, ClientTicketWrite, ClientTicketStatusUpdate } from './dto/client-ticket'
|
||||
import type { HydraCollection } from '~/utils/api'
|
||||
import { extractHydraMembers } from '~/utils/api'
|
||||
|
||||
export function useClientTicketService() {
|
||||
const api = useApi()
|
||||
|
||||
async function getAll(params?: { project?: number; status?: string; submittedBy?: number }): Promise<ClientTicket[]> {
|
||||
const query: Record<string, unknown> = {}
|
||||
if (params?.project) query.project = `/api/projects/${params.project}`
|
||||
if (params?.status) query.status = params.status
|
||||
if (params?.submittedBy) query.submittedBy = `/api/users/${params.submittedBy}`
|
||||
const data = await api.get<HydraCollection<ClientTicket>>('/client_tickets', query)
|
||||
return extractHydraMembers(data)
|
||||
}
|
||||
|
||||
async function getById(id: number): Promise<ClientTicket> {
|
||||
return api.get<ClientTicket>(`/client_tickets/${id}`)
|
||||
}
|
||||
|
||||
async function create(payload: ClientTicketWrite): Promise<ClientTicket> {
|
||||
return api.post<ClientTicket>('/client_tickets', payload as Record<string, unknown>, {
|
||||
toastSuccessKey: 'portal.ticketCreated',
|
||||
})
|
||||
}
|
||||
|
||||
async function updateStatus(id: number, payload: ClientTicketStatusUpdate): Promise<ClientTicket> {
|
||||
return api.patch<ClientTicket>(`/client_tickets/${id}`, payload as Record<string, unknown>, {
|
||||
toastSuccessKey: 'clientTicket.statusUpdated',
|
||||
})
|
||||
}
|
||||
|
||||
async function update(id: number, data: Partial<ClientTicketWrite>): Promise<ClientTicket> {
|
||||
return api.patch<ClientTicket>(`/client_tickets/${id}`, data as Record<string, unknown>, {
|
||||
toastSuccessKey: 'clientTicket.updated',
|
||||
})
|
||||
}
|
||||
|
||||
async function remove(id: number): Promise<void> {
|
||||
await api.delete(`/client_tickets/${id}`, {}, {
|
||||
toastSuccessKey: 'clientTicket.deleted',
|
||||
})
|
||||
}
|
||||
|
||||
return { getAll, getById, create, update, updateStatus, remove }
|
||||
}
|
||||
93
frontend/services/dto/absence.ts
Normal file
93
frontend/services/dto/absence.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
export type AbsenceType = 'cp' | 'mariage_pacs' | '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
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { TaskDocument } from './task-document'
|
||||
|
||||
export type ClientTicketType = 'bug' | 'improvement' | 'other'
|
||||
export type ClientTicketStatus = 'new' | 'in_progress' | 'done' | 'rejected'
|
||||
|
||||
export type ClientTicket = {
|
||||
'@id'?: string
|
||||
id: number
|
||||
number: number
|
||||
type: ClientTicketType
|
||||
title: string
|
||||
description: string
|
||||
url: string | null
|
||||
status: ClientTicketStatus
|
||||
statusComment: string | null
|
||||
project: string
|
||||
submittedBy: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
documents?: TaskDocument[]
|
||||
}
|
||||
|
||||
export type ClientTicketWrite = {
|
||||
type: ClientTicketType
|
||||
title: string
|
||||
description: string
|
||||
url?: string | null
|
||||
project: string
|
||||
}
|
||||
|
||||
export type ClientTicketStatusUpdate = {
|
||||
status: ClientTicketStatus
|
||||
statusComment?: string | null
|
||||
}
|
||||
@@ -7,7 +7,6 @@ export type Notification = {
|
||||
type: NotificationType
|
||||
title: string
|
||||
message: string
|
||||
relatedTicket: string | null
|
||||
isRead: boolean
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
@@ -23,13 +23,6 @@ export type Task = {
|
||||
tags: TaskTag[]
|
||||
documents: TaskDocument[]
|
||||
archived: boolean
|
||||
clientTicket: {
|
||||
id: number
|
||||
number: number
|
||||
type: string
|
||||
status: string
|
||||
title: string
|
||||
} | null
|
||||
scheduledStart: string | null
|
||||
scheduledEnd: string | null
|
||||
deadline: string | null
|
||||
@@ -61,7 +54,6 @@ export type TaskWrite = {
|
||||
project: string
|
||||
tags: string[]
|
||||
archived?: boolean
|
||||
clientTicket?: string | null
|
||||
scheduledStart?: string | null
|
||||
scheduledEnd?: string | null
|
||||
deadline?: string | null
|
||||
|
||||
@@ -1,20 +1,39 @@
|
||||
import type { Project } from './project'
|
||||
export type ContractType = 'CDI' | 'CDD' | 'STAGE' | 'ALTERNANCE' | 'AUTRE'
|
||||
export type FamilySituation = 'CELIBATAIRE' | 'MARIE' | 'PACSE' | 'DIVORCE' | 'VEUF'
|
||||
|
||||
export type UserData = {
|
||||
id: number
|
||||
'@id'?: string
|
||||
username: string
|
||||
roles: string[]
|
||||
client?: { id: number; name: string } | null
|
||||
allowedProjects?: Project[]
|
||||
avatarUrl?: string | null
|
||||
apiToken?: string | null
|
||||
// HR / absence management
|
||||
isEmployee?: boolean
|
||||
hireDate?: string | null
|
||||
endDate?: string | null
|
||||
contractType?: ContractType | null
|
||||
workTimeRatio?: number
|
||||
annualLeaveDays?: number
|
||||
referencePeriodStart?: string
|
||||
initialLeaveBalance?: number
|
||||
familySituation?: FamilySituation | null
|
||||
nbChildren?: number
|
||||
}
|
||||
|
||||
export type UserWrite = {
|
||||
username: string
|
||||
plainPassword?: string
|
||||
roles: string[]
|
||||
client?: string | null
|
||||
allowedProjects?: string[]
|
||||
// HR / absence management
|
||||
isEmployee?: boolean
|
||||
hireDate?: string | null
|
||||
endDate?: string | null
|
||||
contractType?: ContractType | null
|
||||
workTimeRatio?: number
|
||||
annualLeaveDays?: number
|
||||
referencePeriodStart?: string
|
||||
initialLeaveBalance?: number
|
||||
familySituation?: FamilySituation | null
|
||||
nbChildren?: number
|
||||
}
|
||||
|
||||
@@ -31,17 +31,6 @@ export function useTaskDocumentService() {
|
||||
return uploadWithRelation('task', `/api/tasks/${taskId}`, file)
|
||||
}
|
||||
|
||||
async function uploadForTicket(clientTicketId: number, file: File): Promise<TaskDocument> {
|
||||
return uploadWithRelation('clientTicket', `/api/client_tickets/${clientTicketId}`, file)
|
||||
}
|
||||
|
||||
async function getByTicket(clientTicketId: number): Promise<TaskDocument[]> {
|
||||
const data = await api.get<HydraCollection<TaskDocument>>('/task_documents', {
|
||||
clientTicket: `/api/client_tickets/${clientTicketId}`,
|
||||
})
|
||||
return extractHydraMembers(data)
|
||||
}
|
||||
|
||||
async function remove(id: number): Promise<void> {
|
||||
await api.delete(`/task_documents/${id}`, {}, {
|
||||
toastSuccessKey: 'taskDocuments.deleted',
|
||||
@@ -52,5 +41,5 @@ export function useTaskDocumentService() {
|
||||
return `${baseURL}/task_documents/${id}/download`
|
||||
}
|
||||
|
||||
return { getByTask, upload, uploadForTicket, getByTicket, remove, getDownloadUrl }
|
||||
return { getByTask, upload, remove, getDownloadUrl }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user