Skip to content

Commit d94820c

Browse files
feat(mimoquage): read-only fields + navigation through saved steps (#3252)
Co-authored-by: Gary van Woerkens <gary.van-woerkens@sg.social.gouv.fr>
1 parent ebe08f4 commit d94820c

18 files changed

Lines changed: 188 additions & 8 deletions

packages/app/src/e2e/declaration-process-panel.e2e.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ test.describe("Declaration process panel", () => {
120120
).toBeVisible();
121121

122122
const ctaLink = panel.getByRole("link", {
123-
name: "Commencer la déclaration",
123+
name: "Continuer la déclaration",
124124
});
125125
await expect(ctaLink).toHaveAttribute("href", /evaluation-conjointe/);
126126
});
@@ -149,7 +149,7 @@ test.describe("Declaration process panel", () => {
149149
).toBeVisible();
150150

151151
const ctaLink = panel.getByRole("link", {
152-
name: "Commencer la déclaration",
152+
name: "Continuer la déclaration",
153153
});
154154
await expect(ctaLink).toHaveAttribute("href", /avis-cse/);
155155
});

packages/app/src/modules/declaration-remuneration/shared/FormActions.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ type FormActionsProps = {
1212
isSubmitting?: boolean;
1313
nextDisabled?: boolean;
1414
className?: string;
15+
/**
16+
* URL used by the "Suivant" button while admin impersonation is active.
17+
* - Provided (data already saved) → button is rendered as a Link so the admin
18+
* can navigate without triggering the submit/save mutation.
19+
* - Omitted (data never saved) → button stays disabled, since navigating
20+
* forward without any saved data would skip a step (issue #3230).
21+
* Ignored when `nextHref` is set (the button is already a Link).
22+
*/
23+
mimoquageNextHref?: string;
1524
};
1625

1726
export function FormActions({
@@ -21,6 +30,7 @@ export function FormActions({
2130
isSubmitting = false,
2231
nextDisabled = false,
2332
className,
33+
mimoquageNextHref,
2434
}: FormActionsProps) {
2535
const { isReadOnly, buttonProps, tooltip } = useReadOnlyGuard();
2636

@@ -43,6 +53,17 @@ export function FormActions({
4353
>
4454
{nextLabel}
4555
</Link>
56+
) : isReadOnly && mimoquageNextHref ? (
57+
<span>
58+
<Link
59+
aria-describedby={buttonProps["aria-describedby"]}
60+
className="fr-btn fr-icon-arrow-right-line fr-btn--icon-right"
61+
href={mimoquageNextHref}
62+
>
63+
{nextLabel}
64+
</Link>
65+
{tooltip}
66+
</span>
4667
) : (
4768
<span>
4869
<button

packages/app/src/modules/declaration-remuneration/shared/PayGapTable.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type PayGapTableProps = {
3838
rows: PayGapRow[];
3939
onRowChange: (index: number, field: PayGapField, value: string) => void;
4040
className?: string;
41+
disabled?: boolean;
4142
};
4243

4344
export function PayGapTable({
@@ -46,6 +47,7 @@ export function PayGapTable({
4647
rows,
4748
onRowChange,
4849
className,
50+
disabled = false,
4951
}: PayGapTableProps) {
5052
return (
5153
<div
@@ -84,6 +86,7 @@ export function PayGapTable({
8486
<input
8587
aria-label={`${row.label} — Femmes`}
8688
className="fr-input"
89+
disabled={disabled}
8790
inputMode="decimal"
8891
onChange={(e) =>
8992
onRowChange(i, "womenValue", e.target.value)
@@ -99,6 +102,7 @@ export function PayGapTable({
99102
<input
100103
aria-label={`${row.label} — Hommes`}
101104
className="fr-input"
105+
disabled={disabled}
102106
inputMode="decimal"
103107
onChange={(e) =>
104108
onRowChange(i, "menValue", e.target.value)

packages/app/src/modules/declaration-remuneration/shared/__tests__/FormActions.test.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ describe("FormActions", () => {
5252
mockedUseSession.mockReset();
5353
});
5454

55-
it("disables the submit button and renders a tooltip when impersonating", () => {
55+
function mockImpersonating() {
5656
mockedUseSession.mockReturnValue({
5757
data: {
5858
user: {
@@ -63,6 +63,10 @@ describe("FormActions", () => {
6363
},
6464
status: "authenticated",
6565
} as unknown as ReturnType<typeof useSession>);
66+
}
67+
68+
it("disables the submit button and renders a tooltip when impersonating without a saved record", () => {
69+
mockImpersonating();
6670

6771
render(<FormActions />);
6872

@@ -73,6 +77,32 @@ describe("FormActions", () => {
7377
expect(button).toHaveAttribute("aria-describedby", tooltip.id);
7478
});
7579

80+
it("renders a Link instead of the submit button when impersonating with mimoquageNextHref", () => {
81+
mockImpersonating();
82+
83+
render(
84+
<FormActions mimoquageNextHref="/declaration-remuneration/etape/2" />,
85+
);
86+
87+
expect(
88+
screen.queryByRole("button", { name: /suivant/i }),
89+
).not.toBeInTheDocument();
90+
const link = screen.getByRole("link", { name: /suivant/i });
91+
expect(link).toHaveAttribute("href", "/declaration-remuneration/etape/2");
92+
const tooltip = screen.getByRole("tooltip");
93+
expect(link).toHaveAttribute("aria-describedby", tooltip.id);
94+
});
95+
96+
it("ignores mimoquageNextHref when not impersonating", () => {
97+
render(
98+
<FormActions mimoquageNextHref="/declaration-remuneration/etape/2" />,
99+
);
100+
101+
const button = screen.getByRole("button", { name: /suivant/i });
102+
expect(button).toHaveAttribute("type", "submit");
103+
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
104+
});
105+
76106
it("does not render the tooltip when not impersonating", () => {
77107
render(<FormActions />);
78108

packages/app/src/modules/declaration-remuneration/steps/CompliancePathChoice.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useRouter } from "next/navigation";
44
import { Controller } from "react-hook-form";
5+
import { useIsImpersonating } from "~/modules/auth";
56
import { saveCompliancePathSchema } from "~/modules/declaration-remuneration/schemas";
67
import type { CampaignDeadlines } from "~/modules/domain";
78
import { NewTabNotice } from "~/modules/layout/shared/NewTabNotice";
@@ -27,17 +28,20 @@ type Props = {
2728
function JointEvaluationOption({
2829
checked,
2930
deadline,
31+
disabled,
3032
onChange,
3133
}: {
3234
checked: boolean;
3335
deadline: Date;
36+
disabled?: boolean;
3437
onChange: () => void;
3538
}) {
3639
return (
3740
<div className="fr-fieldset__element">
3841
<CompliancePathOption
3942
checked={checked}
4043
deadline={deadline}
44+
disabled={disabled}
4145
id="path-joint"
4246
learnMoreHref="https://travail-emploi.gouv.fr/droit-du-travail/egalite-professionnelle"
4347
learnMoreLabel="En savoir plus sur évaluation conjointe des rémunérations"
@@ -69,17 +73,20 @@ function JointEvaluationOption({
6973
function JustifyOption({
7074
checked,
7175
deadline,
76+
disabled,
7277
onChange,
7378
}: {
7479
checked: boolean;
7580
deadline: Date;
81+
disabled?: boolean;
7682
onChange: () => void;
7783
}) {
7884
return (
7985
<div className="fr-fieldset__element">
8086
<CompliancePathOption
8187
checked={checked}
8288
deadline={deadline}
89+
disabled={disabled}
8390
id="path-justify"
8491
name="compliance-path"
8592
onChange={onChange}
@@ -100,11 +107,13 @@ function JustifyOption({
100107
}
101108

102109
function SecondRoundOptions({
110+
disabled,
103111
justificationDeadline,
104112
jointEvaluationDeadline,
105113
selectedPath,
106114
setSelectedPath,
107115
}: {
116+
disabled?: boolean;
108117
justificationDeadline: Date;
109118
jointEvaluationDeadline: Date;
110119
selectedPath: CompliancePathValue | undefined;
@@ -115,11 +124,13 @@ function SecondRoundOptions({
115124
<JustifyOption
116125
checked={selectedPath === "justify"}
117126
deadline={justificationDeadline}
127+
disabled={disabled}
118128
onChange={() => setSelectedPath("justify")}
119129
/>
120130
<JointEvaluationOption
121131
checked={selectedPath === "joint_evaluation"}
122132
deadline={jointEvaluationDeadline}
133+
disabled={disabled}
123134
onChange={() => setSelectedPath("joint_evaluation")}
124135
/>
125136
</>
@@ -128,12 +139,14 @@ function SecondRoundOptions({
128139

129140
function FirstRoundOptions({
130141
correctiveActionDeadline,
142+
disabled,
131143
jointEvaluationDeadline,
132144
justificationDeadline,
133145
selectedPath,
134146
setSelectedPath,
135147
}: {
136148
correctiveActionDeadline: Date;
149+
disabled?: boolean;
137150
jointEvaluationDeadline: Date;
138151
justificationDeadline: Date;
139152
selectedPath: CompliancePathValue | undefined;
@@ -144,6 +157,7 @@ function FirstRoundOptions({
144157
<JustifyOption
145158
checked={selectedPath === "justify"}
146159
deadline={justificationDeadline}
160+
disabled={disabled}
147161
onChange={() => setSelectedPath("justify")}
148162
/>
149163

@@ -156,6 +170,7 @@ function FirstRoundOptions({
156170
<CompliancePathOption
157171
checked={selectedPath === "corrective_action"}
158172
deadline={correctiveActionDeadline}
173+
disabled={disabled}
159174
id="path-corrective"
160175
learnMoreHref="https://travail-emploi.gouv.fr/droit-du-travail/egalite-professionnelle"
161176
learnMoreLabel="En savoir plus sur actions correctives et seconde déclaration"
@@ -194,12 +209,23 @@ function FirstRoundOptions({
194209
<JointEvaluationOption
195210
checked={selectedPath === "joint_evaluation"}
196211
deadline={jointEvaluationDeadline}
212+
disabled={disabled}
197213
onChange={() => setSelectedPath("joint_evaluation")}
198214
/>
199215
</>
200216
);
201217
}
202218

219+
function getCompliancePathHref(path: CompliancePathValue): string {
220+
if (path === "corrective_action") {
221+
return "/declaration-remuneration/parcours-conformite/etape/1";
222+
}
223+
if (path === "joint_evaluation") {
224+
return "/declaration-remuneration/parcours-conformite/evaluation-conjointe";
225+
}
226+
return "/avis-cse";
227+
}
228+
203229
export function CompliancePathChoice({
204230
campaignDeadlines,
205231
currentYear,
@@ -209,6 +235,7 @@ export function CompliancePathChoice({
209235
pdfDownloadHref,
210236
}: Props) {
211237
const router = useRouter();
238+
const isImpersonating = useIsImpersonating();
212239

213240
const form = useZodForm(saveCompliancePathSchema, {
214241
defaultValues: { path: initialPath },
@@ -300,6 +327,7 @@ export function CompliancePathChoice({
300327

301328
{isSecondRound ? (
302329
<SecondRoundOptions
330+
disabled={isImpersonating}
303331
jointEvaluationDeadline={
304332
campaignDeadlines.decl2JointEvaluationDeadline
305333
}
@@ -314,6 +342,7 @@ export function CompliancePathChoice({
314342
correctiveActionDeadline={
315343
campaignDeadlines.decl2ModificationDeadline
316344
}
345+
disabled={isImpersonating}
317346
jointEvaluationDeadline={
318347
campaignDeadlines.decl1JointEvaluationDeadline
319348
}
@@ -330,6 +359,9 @@ export function CompliancePathChoice({
330359

331360
<FormActions
332361
isSubmitting={mutation.isPending}
362+
mimoquageNextHref={
363+
initialPath ? getCompliancePathHref(initialPath) : undefined
364+
}
333365
nextDisabled={!selectedPath}
334366
nextLabel="Suivant"
335367
previousHref="/declaration-remuneration/etape/6"

packages/app/src/modules/declaration-remuneration/steps/Step1Workforce.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useRouter } from "next/navigation";
44
import { useState } from "react";
55

6+
import { useIsImpersonating } from "~/modules/auth";
67
import { useZodForm } from "~/modules/shared/useZodForm";
78
import { api } from "~/trpc/react";
89
import { updateStep1Schema } from "../schemas";
@@ -30,6 +31,7 @@ export function Step1Workforce({
3031
gipPrefillData,
3132
}: Step1WorkforceProps) {
3233
const router = useRouter();
34+
const isImpersonating = useIsImpersonating();
3335
const isPrefilled = !!gipPrefillData;
3436

3537
const hasInitialData = initialData.totalWomen > 0 || initialData.totalMen > 0;
@@ -165,6 +167,7 @@ export function Step1Workforce({
165167
<input
166168
aria-label="Nombre de femmes"
167169
className="fr-input"
170+
disabled={isImpersonating}
168171
inputMode="numeric"
169172
onChange={handleWomenChange}
170173
pattern="[0-9]*"
@@ -176,6 +179,7 @@ export function Step1Workforce({
176179
<input
177180
aria-label="Nombre d'hommes"
178181
className="fr-input"
182+
disabled={isImpersonating}
179183
inputMode="numeric"
180184
onChange={handleMenChange}
181185
pattern="[0-9]*"
@@ -216,6 +220,9 @@ export function Step1Workforce({
216220
<FormActions
217221
className="fr-mt-0"
218222
isSubmitting={mutation.isPending}
223+
mimoquageNextHref={
224+
hasInitialData ? "/declaration-remuneration/etape/2" : undefined
225+
}
219226
previousHref="/"
220227
/>
221228
</form>

packages/app/src/modules/declaration-remuneration/steps/Step2PayGap.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useRouter } from "next/navigation";
44
import { useState } from "react";
55

6+
import { useIsImpersonating } from "~/modules/auth";
67
import { normalizeDecimalInput } from "~/modules/domain";
78
import { useZodForm } from "~/modules/shared/useZodForm";
89
import { api } from "~/trpc/react";
@@ -35,6 +36,7 @@ export function Step2PayGap({
3536
gipPrefillData,
3637
}: Step2PayGapProps) {
3738
const router = useRouter();
39+
const isImpersonating = useIsImpersonating();
3840

3941
const hasSavedData = Object.values(initialData).some((v) => v !== "");
4042
const defaultValues = hasSavedData
@@ -131,6 +133,7 @@ export function Step2PayGap({
131133
<PayGapTable
132134
caption="Écart de rémunération"
133135
columnHeader="Rémunération"
136+
disabled={isImpersonating}
134137
onRowChange={handleRowChange}
135138
rows={rows}
136139
/>
@@ -159,6 +162,9 @@ export function Step2PayGap({
159162
<FormActions
160163
className="fr-mt-0"
161164
isSubmitting={mutation.isPending}
165+
mimoquageNextHref={
166+
hasSavedData ? "/declaration-remuneration/etape/3" : undefined
167+
}
162168
previousHref="/declaration-remuneration/etape/1"
163169
/>
164170
</form>

0 commit comments

Comments
 (0)