@@ -9,103 +9,118 @@ function getDaysInMonth(month: number, year: number): number {
99}
1010
1111/**
12- * Schéma de validation pour la création/édition d'un CRA mensuel
12+ * Valide qu'un tableau de jours n'a pas de doublons
13+ */
14+ function hasNoDuplicateDays ( days : number [ ] ) : boolean {
15+ return new Set ( days ) . size === days . length ;
16+ }
17+
18+ /**
19+ * Valide que tous les jours sont dans la plage 1-31
20+ */
21+ function allDaysInValidRange ( days : number [ ] ) : boolean {
22+ return days . every ( ( day ) => day >= 1 && day <= 31 ) ;
23+ }
24+
25+ /**
26+ * Valide que tous les jours sont valides pour un mois/année donnés
27+ */
28+ function allDaysValidForMonth (
29+ days : number [ ] ,
30+ month : number ,
31+ year : number ,
32+ ) : boolean {
33+ const daysInMonth = getDaysInMonth ( month , year ) ;
34+ return days . every ( ( day ) => day <= daysInMonth ) ;
35+ }
36+
37+ /**
38+ * Expression régulière pour valider un UUID
39+ */
40+ const UUID_REGEX =
41+ / ^ [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 12 } $ / i;
42+
43+ /**
44+ * Schéma de base pour un CRA (sans worked_days ni refinements cross-field)
45+ * Utilisé pour dériver les schémas complet et draft
1346 */
14- export const craFormSchema = z
15- . object ( {
16- month : z
17- . number ( { message : 'Le mois est requis' } )
18- . int ( 'Le mois doit être un nombre entier' )
19- . min ( 1 , 'Le mois doit être entre 1 et 12' )
20- . max ( 12 , 'Le mois doit être entre 1 et 12' ) ,
21- year : z
22- . number ( { message : "L'année est requise" } )
23- . int ( "L'année doit être un nombre entier" )
24- . min ( 2000 , "L'année doit être supérieure ou égale à 2000" )
25- . max ( 2100 , "L'année doit être inférieure ou égale à 2100" )
26- . refine (
27- ( year ) => {
28- // Ne pas permettre les années trop lointaines dans le futur
29- const currentYear = new Date ( ) . getFullYear ( ) ;
30- return year <= currentYear + 5 ;
31- } ,
32- { message : "L'année ne peut pas être trop éloignée dans le futur" } ,
33- ) ,
34- worked_days : z
35- . array ( z . number ( ) . int ( 'Les jours doivent être des nombres entiers' ) )
36- . min ( 1 , 'Au moins un jour travaillé est requis' )
37- . max ( 31 , 'Le nombre de jours travaillés ne peut pas dépasser 31' )
38- . refine (
39- ( days ) => {
40- // Vérifier qu'il n'y a pas de doublons
41- const uniqueDays = new Set ( days ) ;
42- return uniqueDays . size === days . length ;
43- } ,
44- { message : 'Les jours travaillés ne doivent pas contenir de doublons' } ,
45- )
46- . refine (
47- ( days ) => {
48- // Vérifier que tous les jours sont dans la plage 1-31
49- return days . every ( ( day ) => day >= 1 && day <= 31 ) ;
50- } ,
51- { message : 'Les jours doivent être entre 1 et 31' } ,
52- ) ,
53- comment : z
54- . string ( )
55- . max ( 1000 , 'Le commentaire ne peut pas dépasser 1000 caractères' )
56- . optional ( ) ,
57- client_id : z
58- . string ( )
59- . min ( 1 , 'Le client est requis' )
60- . refine (
61- ( val ) => {
62- // Si la valeur est vide, c'est déjà géré par .min(1)
63- // Sinon, vérifier que c'est un UUID valide
64- if ( val . length === 0 ) return false ;
65- return / ^ [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 12 } $ / i. test (
66- val ,
67- ) ;
68- } ,
69- { message : 'Le client sélectionné est invalide' } ,
70- ) ,
71- provider_id : z
72- . string ( )
73- . min ( 1 , 'Le prestataire est requis' )
74- . refine (
75- ( val ) => {
76- // Si la valeur est vide, c'est déjà géré par .min(1)
77- // Sinon, vérifier que c'est un UUID valide
78- if ( val . length === 0 ) return false ;
79- return / ^ [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 4 } - [ 0 - 9 a - f ] { 12 } $ / i. test (
80- val ,
81- ) ;
82- } ,
83- { message : 'Le prestataire sélectionné est invalide' } ,
84- ) ,
85- status : z . enum ( CRA_STATUSES ) . optional ( ) . default ( 'draft' ) ,
47+ const craFormBaseSchema = z . object ( {
48+ month : z
49+ . number ( { message : 'Le mois est requis' } )
50+ . int ( 'Le mois doit être un nombre entier' )
51+ . min ( 1 , 'Le mois doit être entre 1 et 12' )
52+ . max ( 12 , 'Le mois doit être entre 1 et 12' ) ,
53+ year : z
54+ . number ( { message : "L'année est requise" } )
55+ . int ( "L'année doit être un nombre entier" )
56+ . min ( 2000 , "L'année doit être supérieure ou égale à 2000" )
57+ . max ( 2100 , "L'année doit être inférieure ou égale à 2100" )
58+ . refine (
59+ ( year ) => {
60+ const currentYear = new Date ( ) . getFullYear ( ) ;
61+ return year <= currentYear + 5 ;
62+ } ,
63+ { message : "L'année ne peut pas être trop éloignée dans le futur" } ,
64+ ) ,
65+ comment : z
66+ . string ( )
67+ . max ( 1000 , 'Le commentaire ne peut pas dépasser 1000 caractères' )
68+ . optional ( ) ,
69+ client_id : z
70+ . string ( )
71+ . min ( 1 , 'Le client est requis' )
72+ . refine ( ( val ) => val . length === 0 || UUID_REGEX . test ( val ) , {
73+ message : 'Le client sélectionné est invalide' ,
74+ } ) ,
75+ provider_id : z
76+ . string ( )
77+ . min ( 1 , 'Le prestataire est requis' )
78+ . refine ( ( val ) => val . length === 0 || UUID_REGEX . test ( val ) , {
79+ message : 'Le prestataire sélectionné est invalide' ,
80+ } ) ,
81+ status : z . enum ( CRA_STATUSES ) . optional ( ) . default ( 'draft' ) ,
82+
83+ // Signatures (optionnelles, overrides des signatures par défaut)
84+ client_signatory_name : z . string ( ) . max ( 255 ) . optional ( ) ,
85+ client_signatory_title : z . string ( ) . max ( 255 ) . optional ( ) ,
86+ client_signature_image : z . string ( ) . optional ( ) ,
87+ client_signature_location : z . string ( ) . max ( 255 ) . optional ( ) ,
88+ client_use_current_date : z . boolean ( ) . optional ( ) ,
89+ provider_signatory_name : z . string ( ) . max ( 255 ) . optional ( ) ,
90+ provider_signatory_title : z . string ( ) . max ( 255 ) . optional ( ) ,
91+ provider_signature_image : z . string ( ) . optional ( ) ,
92+ provider_signature_location : z . string ( ) . max ( 255 ) . optional ( ) ,
93+ provider_use_current_date : z . boolean ( ) . optional ( ) ,
94+ } ) ;
8695
87- // Signatures (optionnelles, overrides des signatures par défaut)
88- client_signatory_name : z . string ( ) . max ( 255 ) . optional ( ) ,
89- client_signatory_title : z . string ( ) . max ( 255 ) . optional ( ) ,
90- client_signature_image : z . string ( ) . optional ( ) , // Base64 data URL, pas de limite
91- client_signature_location : z . string ( ) . max ( 255 ) . optional ( ) ,
92- client_use_current_date : z . boolean ( ) . optional ( ) ,
93- provider_signatory_name : z . string ( ) . max ( 255 ) . optional ( ) ,
94- provider_signatory_title : z . string ( ) . max ( 255 ) . optional ( ) ,
95- provider_signature_image : z . string ( ) . optional ( ) , // Base64 data URL, pas de limite
96- provider_signature_location : z . string ( ) . max ( 255 ) . optional ( ) ,
97- provider_use_current_date : z . boolean ( ) . optional ( ) ,
96+ /**
97+ * Schéma pour worked_days avec validations complètes (requis)
98+ */
99+ const workedDaysRequiredSchema = z
100+ . array ( z . number ( ) . int ( 'Les jours doivent être des nombres entiers' ) )
101+ . min ( 1 , 'Au moins un jour travaillé est requis' )
102+ . max ( 31 , 'Le nombre de jours travaillés ne peut pas dépasser 31' )
103+ . refine ( hasNoDuplicateDays , {
104+ message : 'Les jours travaillés ne doivent pas contenir de doublons' ,
105+ } )
106+ . refine ( allDaysInValidRange , {
107+ message : 'Les jours doivent être entre 1 et 31' ,
108+ } ) ;
109+
110+ /**
111+ * Schéma de validation pour la création/édition d'un CRA mensuel
112+ * Inclut les validations cross-field (client != provider, jours valides pour le mois)
113+ */
114+ export const craFormSchema = craFormBaseSchema
115+ . extend ( {
116+ worked_days : workedDaysRequiredSchema ,
98117 } )
99118 . refine ( ( data ) => data . client_id !== data . provider_id , {
100119 message : 'Le client et le prestataire doivent être différents' ,
101120 path : [ 'provider_id' ] ,
102121 } )
103122 . refine (
104- ( data ) => {
105- // Vérifier que tous les jours travaillés sont valides pour le mois sélectionné
106- const daysInMonth = getDaysInMonth ( data . month , data . year ) ;
107- return data . worked_days . every ( ( day ) => day <= daysInMonth ) ;
108- } ,
123+ ( data ) => allDaysValidForMonth ( data . worked_days , data . month , data . year ) ,
109124 {
110125 message : 'Certains jours sélectionnés sont invalides pour le mois choisi' ,
111126 path : [ 'worked_days' ] ,
@@ -119,14 +134,15 @@ export type CRAFormData = z.infer<typeof craFormSchema>;
119134
120135/**
121136 * Validation partielle pour la sauvegarde en brouillon
137+ * - worked_days est optionnel (tableau vide par défaut)
138+ * - client_id et provider_id sont optionnels
139+ * - Pas de validation cross-field
122140 */
123- export const craFormDraftSchema = craFormSchema
124- . partial ( {
125- worked_days : true ,
126- } )
127- . extend ( {
128- worked_days : z . array ( z . number ( ) . int ( ) ) . optional ( ) . default ( [ ] ) ,
129- } ) ;
141+ export const craFormDraftSchema = craFormBaseSchema . extend ( {
142+ worked_days : z . array ( z . number ( ) . int ( ) ) . optional ( ) . default ( [ ] ) ,
143+ client_id : z . string ( ) . optional ( ) . default ( '' ) ,
144+ provider_id : z . string ( ) . optional ( ) . default ( '' ) ,
145+ } ) ;
130146
131147/**
132148 * Type pour le brouillon
0 commit comments