22
33import { useEffect , useState } from "react" ;
44
5+ import { FIRST_DECLARATION_YEAR , getCurrentYear } from "~/modules/domain" ;
56import { useZodForm } from "~/modules/shared/useZodForm" ;
67import { api } from "~/trpc/react" ;
78
@@ -15,12 +16,10 @@ type Props = {
1516 configuredYears : number [ ] ;
1617} ;
1718
18- type DateFieldKey = Exclude < keyof CampaignDeadlinesFormInput , "year" > ;
19-
20- const OPTIONAL_FIELDS : readonly DateFieldKey [ ] = [
21- "gipPublicationDate" ,
22- "campaignStartDate" ,
23- ] ;
19+ type DateFieldKey = Exclude <
20+ keyof CampaignDeadlinesFormInput ,
21+ "year" | "gipPublicationDate"
22+ > ;
2423
2524const DECL1_FIELDS : readonly DateFieldKey [ ] = [
2625 "decl1ModificationDeadline" ,
@@ -35,7 +34,6 @@ const DECL2_FIELDS: readonly DateFieldKey[] = [
3534] ;
3635
3736const FIELD_LABELS : Record < DateFieldKey , string > = {
38- gipPublicationDate : "Date de publication des données GIP" ,
3937 campaignStartDate : "Date de démarrage de la campagne" ,
4038 decl1ModificationDeadline : "Date limite de modification" ,
4139 decl1JustificationDeadline : "Date limite de justification" ,
@@ -47,7 +45,11 @@ const FIELD_LABELS: Record<DateFieldKey, string> = {
4745
4846/**
4947 * Edits all campaign deadlines for a given year. The year selector lets the
50- * admin switch between already-configured years or start a new one.
48+ * admin switch between every year since the platform launched, and a visible
49+ * box around the fieldsets clarifies that they depend on the selected year.
50+ *
51+ * `gipPublicationDate` is displayed read-only: its value is written by the
52+ * GIP MDS CSV import (`horodatage` column) and cannot be changed from the UI.
5153 */
5254export function CampaignDeadlinesForm ( { initialYear, configuredYears } : Props ) {
5355 const [ selectedYear , setSelectedYear ] = useState < number > ( initialYear ) ;
@@ -68,7 +70,6 @@ export function CampaignDeadlinesForm({ initialYear, configuredYears }: Props) {
6870 if ( ! deadlinesQuery . data ) return ;
6971 form . reset ( {
7072 year : selectedYear ,
71- gipPublicationDate : deadlinesQuery . data . gipPublicationDate ?? "" ,
7273 campaignStartDate : deadlinesQuery . data . campaignStartDate ?? "" ,
7374 decl1ModificationDeadline : deadlinesQuery . data . decl1ModificationDeadline ,
7475 decl1JustificationDeadline :
@@ -103,13 +104,18 @@ export function CampaignDeadlinesForm({ initialYear, configuredYears }: Props) {
103104 mutation . mutate ( values ) ;
104105 } ) ;
105106
106- const yearOptions = buildYearOptions ( configuredYears , selectedYear ) ;
107+ const yearOptions = buildYearOptions ( configuredYears ) ;
108+ const gipPublicationDate = deadlinesQuery . data ?. gipPublicationDate ?? null ;
107109
108110 return (
109111 < >
110112 < div className = "fr-select-group fr-mb-3w" >
111113 < label className = "fr-label" htmlFor = "campaign-year-selector" >
112- Année à éditer
114+ Sélectionnez l'année de campagne à modifier
115+ < span className = "fr-hint-text" >
116+ Choisissez une année dans la liste. Les champs ci-dessous
117+ correspondent à l'année sélectionnée.
118+ </ span >
113119 </ label >
114120 < select
115121 className = "fr-select"
@@ -118,8 +124,8 @@ export function CampaignDeadlinesForm({ initialYear, configuredYears }: Props) {
118124 value = { selectedYear }
119125 >
120126 { yearOptions . map ( ( y ) => (
121- < option key = { y } value = { y } >
122- { y }
127+ < option key = { y . year } value = { y . year } >
128+ { y . label }
123129 </ option >
124130 ) ) }
125131 </ select >
@@ -131,53 +137,65 @@ export function CampaignDeadlinesForm({ initialYear, configuredYears }: Props) {
131137 { ...form . register ( "year" , { valueAsNumber : true } ) }
132138 />
133139
134- < fieldset className = "fr-fieldset" >
135- < legend className = "fr-fieldset__legend" > Campagne</ legend >
136- < div className = "fr-fieldset__content fr-grid-row fr-grid-row--gutters" >
137- { OPTIONAL_FIELDS . map ( ( key ) => (
138- < div className = "fr-col-12 fr-col-md-6" key = { key } >
140+ < div className = "fr-p-3w fr-background-alt--grey" >
141+ < p className = "fr-text--sm fr-text-mention--grey fr-mb-2w" >
142+ Paramètres applicables à la campagne < strong > { selectedYear } </ strong >
143+ .
144+ </ p >
145+
146+ < fieldset className = "fr-fieldset" >
147+ < legend className = "fr-fieldset__legend" > Campagne</ legend >
148+ < div className = "fr-fieldset__content fr-grid-row fr-grid-row--gutters" >
149+ < div className = "fr-col-12 fr-col-md-6" >
150+ < GipPublicationReadOnly value = { gipPublicationDate } />
151+ </ div >
152+ < div className = "fr-col-12 fr-col-md-6" >
139153 < DateField
140- error = { form . formState . errors [ key ] ?. message }
141- fieldKey = { key }
142- register = { form . register ( key ) }
154+ error = { form . formState . errors . campaignStartDate ?. message }
155+ fieldKey = "campaignStartDate"
156+ register = { form . register ( "campaignStartDate" ) }
143157 required = { false }
144158 />
145159 </ div >
146- ) ) }
147- </ div >
148- </ fieldset >
160+ </ div >
161+ </ fieldset >
149162
150- < fieldset className = "fr-fieldset" >
151- < legend className = "fr-fieldset__legend" > Première déclaration</ legend >
152- < div className = "fr-fieldset__content fr-grid-row fr-grid-row--gutters" >
153- { DECL1_FIELDS . map ( ( key ) => (
154- < div className = "fr-col-12 fr-col-md-4" key = { key } >
155- < DateField
156- error = { form . formState . errors [ key ] ?. message }
157- fieldKey = { key }
158- register = { form . register ( key ) }
159- required = { true }
160- />
161- </ div >
162- ) ) }
163- </ div >
164- </ fieldset >
163+ < fieldset className = "fr-fieldset" >
164+ < legend className = "fr-fieldset__legend" >
165+ Première déclaration
166+ </ legend >
167+ < div className = "fr-fieldset__content fr-grid-row fr-grid-row--gutters" >
168+ { DECL1_FIELDS . map ( ( key ) => (
169+ < div className = "fr-col-12 fr-col-md-4" key = { key } >
170+ < DateField
171+ error = { form . formState . errors [ key ] ?. message }
172+ fieldKey = { key }
173+ register = { form . register ( key ) }
174+ required = { true }
175+ />
176+ </ div >
177+ ) ) }
178+ </ div >
179+ </ fieldset >
165180
166- < fieldset className = "fr-fieldset" >
167- < legend className = "fr-fieldset__legend" > Deuxième déclaration</ legend >
168- < div className = "fr-fieldset__content fr-grid-row fr-grid-row--gutters" >
169- { DECL2_FIELDS . map ( ( key ) => (
170- < div className = "fr-col-12 fr-col-md-4" key = { key } >
171- < DateField
172- error = { form . formState . errors [ key ] ?. message }
173- fieldKey = { key }
174- register = { form . register ( key ) }
175- required = { true }
176- />
177- </ div >
178- ) ) }
179- </ div >
180- </ fieldset >
181+ < fieldset className = "fr-fieldset" >
182+ < legend className = "fr-fieldset__legend" >
183+ Deuxième déclaration
184+ </ legend >
185+ < div className = "fr-fieldset__content fr-grid-row fr-grid-row--gutters" >
186+ { DECL2_FIELDS . map ( ( key ) => (
187+ < div className = "fr-col-12 fr-col-md-4" key = { key } >
188+ < DateField
189+ error = { form . formState . errors [ key ] ?. message }
190+ fieldKey = { key }
191+ register = { form . register ( key ) }
192+ required = { true }
193+ />
194+ </ div >
195+ ) ) }
196+ </ div >
197+ </ fieldset >
198+ </ div >
181199
182200 { status === "success" && (
183201 < div
@@ -248,10 +266,29 @@ function DateField({ fieldKey, register, error, required }: DateFieldProps) {
248266 ) ;
249267}
250268
269+ function GipPublicationReadOnly ( { value } : { value : string | null } ) {
270+ return (
271+ < div className = "fr-input-group" >
272+ < label className = "fr-label" htmlFor = "settings-gipPublicationDate" >
273+ Date de publication des données GIP
274+ < span className = "fr-hint-text" >
275+ Lecture seule — valeur issue du fichier GIP récupéré depuis SUIT.
276+ </ span >
277+ </ label >
278+ < input
279+ className = "fr-input"
280+ id = "settings-gipPublicationDate"
281+ readOnly
282+ type = "text"
283+ value = { value ?? "Non disponible" }
284+ />
285+ </ div >
286+ ) ;
287+ }
288+
251289function buildDefaults ( year : number ) : CampaignDeadlinesFormInput {
252290 return {
253291 year,
254- gipPublicationDate : "" ,
255292 campaignStartDate : "" ,
256293 decl1ModificationDeadline : "" ,
257294 decl1JustificationDeadline : "" ,
@@ -262,14 +299,22 @@ function buildDefaults(year: number): CampaignDeadlinesFormInput {
262299 } ;
263300}
264301
302+ /**
303+ * Lists every year between FIRST_DECLARATION_YEAR and the current year + 1 so
304+ * the admin can pick any campaign, flagging years without a DB row as
305+ * "non configurée" rather than hiding them.
306+ */
265307function buildYearOptions (
266308 configuredYears : readonly number [ ] ,
267- selectedYear : number ,
268- ) : number [ ] {
269- const set = new Set < number > ( configuredYears ) ;
270- set . add ( selectedYear ) ;
271- for ( let offset = - 1 ; offset <= 2 ; offset ++ ) {
272- set . add ( selectedYear + offset ) ;
309+ ) : Array < { year : number ; label : string } > {
310+ const max = getCurrentYear ( ) + 1 ;
311+ const configured = new Set ( configuredYears ) ;
312+ const years : Array < { year : number ; label : string } > = [ ] ;
313+ for ( let y = FIRST_DECLARATION_YEAR ; y <= max ; y ++ ) {
314+ years . push ( {
315+ year : y ,
316+ label : configured . has ( y ) ? String ( y ) : `${ y } (non configurée)` ,
317+ } ) ;
273318 }
274- return Array . from ( set ) . sort ( ( a , b ) => a - b ) ;
319+ return years ;
275320}
0 commit comments