Skip to content

Commit 198524e

Browse files
authored
Merge pull request #1257 from betagouv/fusion_statuts
Fusion statuts
2 parents c0442c9 + 594b7cc commit 198524e

25 files changed

Lines changed: 244 additions & 176 deletions

scripts/stats/refresh.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const getDemandesEnCoursStats = async () => {
7777
const totalDemandes = demandes.length;
7878

7979
const demandesEnCours = demandes.filter((record) =>
80-
[DEMANDE_STATUS.IN_PROGRESS, DEMANDE_STATUS.WORK_IN_PROGRESS, DEMANDE_STATUS.DONE].includes(record.fields.Status as DEMANDE_STATUS)
80+
[DEMANDE_STATUS.RECONTACTED, DEMANDE_STATUS.WORK_IN_PROGRESS, DEMANDE_STATUS.DONE].includes(record.fields.Status as DEMANDE_STATUS)
8181
);
8282

8383
const totalLogements = demandesEnCours.reduce((acc, record) => acc + Number(record.fields.Logement || 1), 0);

src/components/Manager/DemandEmailForm.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,11 @@ function DemandEmailForm(props: Props) {
153153
alreadySent.push(emailContent.object);
154154
const updatedFields: any = {
155155
'Emails envoyés': alreadySent.join('\n'),
156-
'Prise de contact': true, //Prospect recontacté
157156
};
158157
if (emailKey === 'koFarFromNetwok' || emailKey === 'koIndividualHeat' || emailKey === 'koOther') {
159158
updatedFields.Status = DEMANDE_STATUS.UNREALISABLE;
160159
} else if (emailKey === 'askForPieces') {
161-
updatedFields.Status = DEMANDE_STATUS.WAITING;
160+
updatedFields.Status = DEMANDE_STATUS.RECONTACTED;
162161
}
163162
await props.updateDemand(props.currentDemand.id, updatedFields);
164163

src/components/ui/ComboBox.tsx

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ type ComboBoxBaseProps = {
1919
placeholder?: string;
2020
disabled?: boolean;
2121
className?: string;
22+
/** En multi-select, nombre de tags affichés avant de replier le reste en « +N » (défaut 2). */
23+
maxVisibleTags?: number;
2224
};
2325

2426
export type ComboBoxProps =
@@ -38,7 +40,7 @@ export type ComboBoxProps =
3840
*/
3941
const ComboBox = (rawProps: ComboBoxProps) => {
4042
const props = { multiple: false, placeholder: 'Sélectionner…', ...rawProps } satisfies ComboBoxProps;
41-
const { options, value, label, placeholder, disabled, className } = props;
43+
const { options, value, label, placeholder, disabled, className, maxVisibleTags = 2 } = props;
4244

4345
const comboboxId = useId();
4446
const listboxId = useId();
@@ -96,14 +98,26 @@ const ComboBox = (rawProps: ComboBoxProps) => {
9698
}
9799
};
98100

101+
const selectAll = () => {
102+
if (!props.multiple) return;
103+
props.onChange(options.filter((option) => !option.disabled).map((option) => option.key));
104+
};
105+
99106
const isSelected = (key: string) => valueArray.includes(key);
100107

108+
const allSelected = props.multiple && options.length > 0 && options.every((option) => option.disabled || isSelected(option.key));
109+
101110
const displayedText = useMemo(() => {
102111
if (props.multiple) return valueArray.map((key) => options.find((option) => option.key === key)?.label || key).join(', ');
103112
if (!valueArray[0]) return '';
104113
return options.find((option) => option.key === valueArray[0])?.label || valueArray[0];
105114
}, [props.multiple, valueArray, options]);
106115

116+
const hiddenTagsTitle = valueArray
117+
.slice(maxVisibleTags)
118+
.map((key) => options.find((option) => option.key === key)?.label || key)
119+
.join(', ');
120+
107121
const activeOptionId = filteredOptions[highlighted] ? `${listboxId}-option-${filteredOptions[highlighted].key}` : undefined;
108122

109123
return (
@@ -157,25 +171,32 @@ const ComboBox = (rawProps: ComboBoxProps) => {
157171
<div className="flex flex-wrap items-center gap-1 flex-1 min-w-0">
158172
{props.multiple ? (
159173
valueArray.length > 0 ? (
160-
valueArray.map((key) => {
161-
const optionLabel = options.find((option) => option.key === key)?.label || key;
162-
return (
163-
<Tag
164-
key={key}
165-
dismissible
166-
small
167-
nativeButtonProps={{
168-
onClick: (event: React.MouseEvent<HTMLButtonElement>) => {
169-
event.stopPropagation();
170-
unselectOne(key);
171-
},
172-
title: optionLabel,
173-
}}
174-
>
175-
{optionLabel}
174+
<>
175+
{valueArray.slice(0, maxVisibleTags).map((key) => {
176+
const optionLabel = options.find((option) => option.key === key)?.label || key;
177+
return (
178+
<Tag
179+
key={key}
180+
dismissible
181+
small
182+
nativeButtonProps={{
183+
onClick: (event: React.MouseEvent<HTMLButtonElement>) => {
184+
event.stopPropagation();
185+
unselectOne(key);
186+
},
187+
title: optionLabel,
188+
}}
189+
>
190+
{optionLabel}
191+
</Tag>
192+
);
193+
})}
194+
{valueArray.length > maxVisibleTags && (
195+
<Tag small title={hiddenTagsTitle}>
196+
+{valueArray.length - maxVisibleTags}
176197
</Tag>
177-
);
178-
})
198+
)}
199+
</>
179200
) : (
180201
<span className="text-gray-500">{placeholder}</span>
181202
)
@@ -195,7 +216,20 @@ const ComboBox = (rawProps: ComboBoxProps) => {
195216
className="border border-solid border-gray-300 shadow-lg"
196217
>
197218
{props.multiple && (
198-
<div className="p-2 border-b border-solid border-gray-200">
219+
<div className="flex items-center gap-2 p-2 border-b border-solid border-gray-200">
220+
<Button
221+
priority="tertiary"
222+
size="small"
223+
iconId="fr-icon-checkbox-circle-line"
224+
onClick={(event) => {
225+
event.preventDefault();
226+
event.stopPropagation();
227+
selectAll();
228+
}}
229+
disabled={allSelected}
230+
>
231+
Tout sélectionner
232+
</Button>
199233
<Button
200234
priority="tertiary"
201235
size="small"

src/modules/demands/client/Contacted.tsx

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/modules/demands/client/DemandStatusBadge.tsx

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Badge from '@codegouvfr/react-dsfr/Badge';
22

33
import type { DemandStatus } from '@/modules/demands/constants';
44
import { demandStatusDefault } from '@/modules/demands/constants';
5+
import { DEMANDE_STATUS } from '@/types/enum/DemandSatus';
56
import { upperCaseFirstChar } from '@/utils/strings';
67

78
const statusConfig: Record<
@@ -10,29 +11,29 @@ const statusConfig: Record<
1011
className: string;
1112
}
1213
> = {
13-
'En attente de prise en charge': {
14-
className: 'bg-gray-500! text-white!',
14+
[DEMANDE_STATUS.TO_PROCESS]: {
15+
className: 'bg-red-600! text-white!',
1516
},
16-
'En attente d’éléments du prospect': {
17-
className: 'bg-yellow-300! text-black!',
18-
},
19-
'Non réalisable': {
17+
[DEMANDE_STATUS.UNREALISABLE]: {
2018
className: 'bg-destructive! text-white!',
2119
},
22-
'Projet abandonné par le prospect': {
23-
className: 'bg-red-700! text-white!',
20+
[DEMANDE_STATUS.RECONTACTED]: {
21+
className: 'bg-[#0d49fb]! text-white!',
2422
},
25-
Réalisé: {
26-
className: 'bg-[#2ca892]! text-white!',
23+
[DEMANDE_STATUS.COMMERCIAL_PROPOSAL]: {
24+
className: 'bg-yellow-300! text-black!',
2725
},
28-
'Travaux en cours': {
26+
[DEMANDE_STATUS.VOTED]: {
27+
className: 'bg-purple-700! text-white!',
28+
},
29+
[DEMANDE_STATUS.WORK_IN_PROGRESS]: {
2930
className: 'bg-indigo-500! text-white!',
3031
},
31-
'Voté en AG': {
32-
className: 'bg-purple-700! text-white!',
32+
[DEMANDE_STATUS.DONE]: {
33+
className: 'bg-[#2ca892]! text-white!',
3334
},
34-
'Étude en cours': {
35-
className: 'bg-[#0d49fb]! text-white!',
35+
[DEMANDE_STATUS.ABANDONNED]: {
36+
className: 'bg-red-700! text-white!',
3637
},
3738
};
3839

src/modules/demands/client/ReseauxStatsPage.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { NotesCell } from '@/modules/reseaux/client/admin/NotesCell';
2121
import { RemindersCell } from '@/modules/reseaux/client/admin/RemindersCell';
2222
import type { NetworkType } from '@/modules/reseaux/constants';
2323
import trpc from '@/modules/trpc/client';
24+
import { DEMANDE_STATUS } from '@/types/enum/DemandSatus';
2425
import { isDefined } from '@/utils/core';
2526
import cx from '@/utils/cx';
2627
import { objectToURLSearchParams } from '@/utils/network';
@@ -433,12 +434,7 @@ const buildDemandFilters = (
433434
pendingOnly: boolean
434435
): ColumnFiltersState => [
435436
{ id: 'network_id', value: { [`${networkType}:${networkId}`]: true } },
436-
...(pendingOnly
437-
? [
438-
{ id: 'Status', value: { 'En attente de prise en charge': true } },
439-
{ id: 'Prise de contact', value: { false: true, true: false } },
440-
]
441-
: []),
437+
...(pendingOnly ? [{ id: 'Status', value: { [DEMANDE_STATUS.TO_PROCESS]: true } }] : []),
442438
...(isDefined(periodMonths)
443439
? [{ id: 'Date de la demande', value: [dayjs().subtract(periodMonths, 'month').format('YYYY-MM-DD'), null, false] }]
444440
: []),

src/modules/demands/client/Status.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ const Status = ({
2525
<Select
2626
label=""
2727
options={demandStatuses.map((status) => ({
28-
label: status.label,
28+
// Le picto n'est affiché que dans la liste déroulante (pas dans le badge)
29+
label: 'icon' in status ? `${status.icon} ${status.label}` : status.label,
2930
value: status.label /** For now, we use the label as the value as we use legacy values */,
3031
}))}
3132
placeholder="Sélectionner un statut"

src/modules/demands/constants.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from 'zod';
22

33
import type { EligibilityType } from '@/server/services/addresseInformation';
4+
import { DEMANDE_STATUS } from '@/types/enum/DemandSatus';
45
import type { ExtractKeys } from '@/utils/typescript';
56

67
export const zAddRelanceCommentInput = z.object({
@@ -42,20 +43,22 @@ const zSubmitSurveyValues = z
4243

4344
export type SubmitSurveyInput = z.infer<typeof zSubmitSurveyValues>;
4445

46+
// Source unique des statuts : l'enum DEMANDE_STATUS porte les libellés ; cette liste ne fait
47+
// qu'ajouter l'ordre d'affichage et le picto (⚠️ uniquement dans la liste déroulante).
4548
export const demandStatuses = [
46-
{ label: 'En attente de prise en charge', value: 'empty' },
47-
{ label: 'Non réalisable', value: 'unrealisable' },
48-
{ label: 'En attente d’éléments du prospect', value: 'waiting' },
49-
{ label: 'Étude en cours', value: 'in_progress' },
50-
{ label: 'Voté en AG', value: 'voted' },
51-
{ label: 'Travaux en cours', value: 'work_in_progress' },
52-
{ label: 'Réalisé', value: 'done' },
53-
{ label: 'Projet abandonné par le prospect', value: 'abandoned' },
49+
{ icon: '⚠️', label: DEMANDE_STATUS.TO_PROCESS },
50+
{ label: DEMANDE_STATUS.UNREALISABLE },
51+
{ label: DEMANDE_STATUS.RECONTACTED },
52+
{ label: DEMANDE_STATUS.COMMERCIAL_PROPOSAL },
53+
{ label: DEMANDE_STATUS.VOTED },
54+
{ label: DEMANDE_STATUS.WORK_IN_PROGRESS },
55+
{ label: DEMANDE_STATUS.DONE },
56+
{ label: DEMANDE_STATUS.ABANDONNED },
5457
] as const;
5558

5659
export const demandStatusDefault = demandStatuses[0].label;
5760

58-
export type DemandStatus = (typeof demandStatuses)[number]['label'];
61+
export type DemandStatus = DEMANDE_STATUS;
5962

6063
// Zod schema for demand update values - only fields actually used in updateDemand calls
6164
// Analysis based on all updateDemand usage across the codebase
@@ -67,7 +70,6 @@ const zGestionnaireDemandUpdateValues = z
6770

6871
// Status & Contact
6972
Status: z.enum([...demandStatuses.map((s) => s.label), '']),
70-
'Prise de contact': z.boolean(),
7173

7274
// Communication
7375
comment_gestionnaire: z.string().nullable(),
@@ -88,7 +90,6 @@ export const zAdminDemandUpdateValues = z
8890
'Relance à activer': z.boolean(),
8991
'Relance ID': z.string().nullable(),
9092
'Notification envoyé': z.string().nullable(),
91-
'Prise de contact': z.boolean(),
9293
'Recontacté par le gestionnaire': z.enum(['Oui', 'Non', '']),
9394

9495
// Communication

src/modules/demands/server/creation-user.integration.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,14 +223,14 @@ describe('creation-user', () => {
223223

224224
const eligibilityCases: EligibilityCase[] = [
225225
{
226-
expectedOutput: { 'Relance à activer': true, Status: undefined },
226+
expectedOutput: { 'Relance à activer': true, Status: 'À traiter' },
227227
input: { heatingType: 'collectif', isEligible: true },
228-
label: 'éligible + collectif → pas de Status, relance active',
228+
label: 'éligible + collectif → Status "À traiter", relance active',
229229
},
230230
{
231-
expectedOutput: { 'Relance à activer': false, Status: undefined },
231+
expectedOutput: { 'Relance à activer': false, Status: 'À traiter' },
232232
input: { heatingType: 'individuel', isEligible: true },
233-
label: 'éligible + individuel → pas de Status, pas de relance',
233+
label: 'éligible + individuel → Status "À traiter", pas de relance',
234234
},
235235
{
236236
expectedOutput: { 'Relance à activer': false, Status: 'Non réalisable' },

src/modules/demands/server/creation-user.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const createDemand = async (
4646
'ID réseau le plus proche': null,
4747
Logement: nbLogement?.nb_logements ? nbLogement.nb_logements : undefined,
4848
'Relance à activer': values.eligibility.isEligible && values.heatingType === 'collectif',
49-
Status: values.eligibility.isEligible ? undefined : DEMANDE_STATUS.UNREALISABLE,
49+
Status: values.eligibility.isEligible ? DEMANDE_STATUS.TO_PROCESS : DEMANDE_STATUS.UNREALISABLE,
5050
})}::jsonb`,
5151
origin_host: values.origin_host ?? null,
5252
origin_page: values.origin_page ?? null,

0 commit comments

Comments
 (0)