Skip to content

Commit b17ffe1

Browse files
committed
fix(declaration): pad euro inputs to two decimals on blur (#3255)
1 parent 81aa381 commit b17ffe1

7 files changed

Lines changed: 93 additions & 3 deletions

File tree

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
formatGap,
66
gapLevel,
77
normalizeDecimalInput,
8+
padDecimalToTwo,
89
} from "~/modules/domain";
910
import type { PayGapField, PayGapRow } from "../types";
1011
import common from "./common.module.scss";
@@ -88,6 +89,12 @@ export function PayGapTable({
8889
className={`fr-input ${common.numericInput}`}
8990
disabled={disabled}
9091
inputMode="decimal"
92+
onBlur={() => {
93+
const padded = padDecimalToTwo(row.womenValue);
94+
if (padded !== row.womenValue) {
95+
onRowChange(i, "womenValue", padded);
96+
}
97+
}}
9198
onChange={(e) =>
9299
onRowChange(i, "womenValue", e.target.value)
93100
}
@@ -104,6 +111,12 @@ export function PayGapTable({
104111
className={`fr-input ${common.numericInput}`}
105112
disabled={disabled}
106113
inputMode="decimal"
114+
onBlur={() => {
115+
const padded = padDecimalToTwo(row.menValue);
116+
if (padded !== row.menValue) {
117+
onRowChange(i, "menValue", padded);
118+
}
119+
}}
107120
onChange={(e) =>
108121
onRowChange(i, "menValue", e.target.value)
109122
}

packages/app/src/modules/declaration-remuneration/steps/step4/QuartileTable.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
import common from "~/modules/declaration-remuneration/shared/common.module.scss";
44
import { QUARTILE_NAMES } from "~/modules/declaration-remuneration/shared/constants";
55
import type { QuartileData } from "~/modules/declaration-remuneration/types";
6-
import { computePercentage, displayDecimal } from "~/modules/domain";
6+
import {
7+
computePercentage,
8+
displayDecimal,
9+
padDecimalToTwo,
10+
} from "~/modules/domain";
711
import stepStyles from "../Step4QuartileDistribution.module.scss";
812

913
type Props = {
@@ -94,6 +98,13 @@ export function QuartileTable({
9498
className={`fr-input ${common.numericInput}`}
9599
disabled={disabled}
96100
inputMode="decimal"
101+
onBlur={() => {
102+
const current = q.threshold ?? "";
103+
const padded = padDecimalToTwo(current);
104+
if (padded !== current) {
105+
onQuartileChange(i, "threshold", padded);
106+
}
107+
}}
97108
onChange={(e) =>
98109
onQuartileChange(i, "threshold", e.target.value)
99110
}

packages/app/src/modules/declaration-remuneration/steps/step5/CategoryDataTable.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ type Props = {
1919
field: keyof EmployeeCategory,
2020
isInteger: boolean,
2121
) => (e: React.ChangeEvent<HTMLInputElement>) => void;
22+
onDecimalBlur: (index: number, field: keyof EmployeeCategory) => () => void;
2223
disabled?: boolean;
2324
};
2425

2526
export function CategoryDataTable({
2627
category: cat,
2728
categoryIndex: catIndex,
2829
onPositiveNumberChange: pos,
30+
onDecimalBlur: blur,
2931
disabled = false,
3032
}: Props) {
3133
const annualTotalWomen = computeTotal(
@@ -141,6 +143,7 @@ export function CategoryDataTable({
141143
disabled={disabled}
142144
id={id("annual-base-women")}
143145
inputMode="decimal"
146+
onBlur={blur(catIndex, "annualBaseWomen")}
144147
onChange={pos(catIndex, "annualBaseWomen", false)}
145148
type="text"
146149
value={displayInputDecimal(cat.annualBaseWomen)}
@@ -156,6 +159,7 @@ export function CategoryDataTable({
156159
disabled={disabled}
157160
id={id("annual-base-men")}
158161
inputMode="decimal"
162+
onBlur={blur(catIndex, "annualBaseMen")}
159163
onChange={pos(catIndex, "annualBaseMen", false)}
160164
type="text"
161165
value={displayInputDecimal(cat.annualBaseMen)}
@@ -183,6 +187,7 @@ export function CategoryDataTable({
183187
disabled={disabled}
184188
id={id("annual-variable-women")}
185189
inputMode="decimal"
190+
onBlur={blur(catIndex, "annualVariableWomen")}
186191
onChange={pos(catIndex, "annualVariableWomen", false)}
187192
type="text"
188193
value={displayInputDecimal(cat.annualVariableWomen)}
@@ -198,6 +203,7 @@ export function CategoryDataTable({
198203
disabled={disabled}
199204
id={id("annual-variable-men")}
200205
inputMode="decimal"
206+
onBlur={blur(catIndex, "annualVariableMen")}
201207
onChange={pos(catIndex, "annualVariableMen", false)}
202208
type="text"
203209
value={displayInputDecimal(cat.annualVariableMen)}
@@ -242,6 +248,7 @@ export function CategoryDataTable({
242248
disabled={disabled}
243249
id={id("hourly-base-women")}
244250
inputMode="decimal"
251+
onBlur={blur(catIndex, "hourlyBaseWomen")}
245252
onChange={pos(catIndex, "hourlyBaseWomen", false)}
246253
type="text"
247254
value={displayInputDecimal(cat.hourlyBaseWomen)}
@@ -257,6 +264,7 @@ export function CategoryDataTable({
257264
disabled={disabled}
258265
id={id("hourly-base-men")}
259266
inputMode="decimal"
267+
onBlur={blur(catIndex, "hourlyBaseMen")}
260268
onChange={pos(catIndex, "hourlyBaseMen", false)}
261269
type="text"
262270
value={displayInputDecimal(cat.hourlyBaseMen)}
@@ -284,6 +292,7 @@ export function CategoryDataTable({
284292
disabled={disabled}
285293
id={id("hourly-variable-women")}
286294
inputMode="decimal"
295+
onBlur={blur(catIndex, "hourlyVariableWomen")}
287296
onChange={pos(catIndex, "hourlyVariableWomen", false)}
288297
type="text"
289298
value={displayInputDecimal(cat.hourlyVariableWomen)}
@@ -299,6 +308,7 @@ export function CategoryDataTable({
299308
disabled={disabled}
300309
id={id("hourly-variable-men")}
301310
inputMode="decimal"
311+
onBlur={blur(catIndex, "hourlyVariableMen")}
302312
onChange={pos(catIndex, "hourlyVariableMen", false)}
303313
type="text"
304314
value={displayInputDecimal(cat.hourlyVariableMen)}

packages/app/src/modules/declaration-remuneration/steps/step5/CategoryForm.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
EmployeeCategoryRow,
1919
EmployeeCategorySubmitData,
2020
} from "~/modules/declaration-remuneration/types";
21+
import { padDecimalToTwo } from "~/modules/domain";
2122
import { useZodForm } from "~/modules/shared/useZodForm";
2223
import stepStyles from "../Step5EmployeeCategories.module.scss";
2324
import { CategoryDataTable } from "./CategoryDataTable";
@@ -156,6 +157,18 @@ export function CategoryForm({
156157
};
157158
}
158159

160+
function handleDecimalBlur(index: number, field: keyof EmployeeCategory) {
161+
return () => {
162+
const formField = field as Exclude<keyof EmployeeCategory, "id">;
163+
const current = form.getValues(`categories.${index}.${formField}`);
164+
const padded = padDecimalToTwo(current);
165+
if (padded !== current) {
166+
form.setValue(`categories.${index}.${formField}`, padded);
167+
setSaved(false);
168+
}
169+
};
170+
}
171+
159172
function addCategory() {
160173
const empty = createEmptyCategory(nextId());
161174
const formEntry = toFormValues([empty])[0];
@@ -453,6 +466,7 @@ export function CategoryForm({
453466
}
454467
categoryIndex={index}
455468
disabled={disabled}
469+
onDecimalBlur={handleDecimalBlur}
456470
onPositiveNumberChange={handlePositiveNumberChange}
457471
/>
458472
</div>

packages/app/src/modules/domain/__tests__/number.test.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
33
import {
44
displayDecimal,
55
normalizeDecimalInput,
6+
padDecimalToTwo,
67
parseNumber,
78
} from "../shared/number";
89

@@ -64,10 +65,36 @@ describe("displayDecimal", () => {
6465
});
6566

6667
it("adds thousand separator", () => {
67-
expect(displayDecimal("1000")).toBe("1\u202F000");
68+
expect(displayDecimal("1000")).toBe("1 000");
6869
});
6970

7071
it("formats large number with decimals", () => {
71-
expect(displayDecimal("1234567.89")).toBe("1\u202F234\u202F567,89");
72+
expect(displayDecimal("1234567.89")).toBe("1 234 567,89");
73+
});
74+
});
75+
76+
describe("padDecimalToTwo", () => {
77+
it("returns empty string as-is", () => {
78+
expect(padDecimalToTwo("")).toBe("");
79+
});
80+
81+
it("pads an integer with .00", () => {
82+
expect(padDecimalToTwo("100")).toBe("100.00");
83+
});
84+
85+
it("pads a one-digit fraction with trailing zero", () => {
86+
expect(padDecimalToTwo("100.5")).toBe("100.50");
87+
});
88+
89+
it("keeps a two-digit fraction unchanged", () => {
90+
expect(padDecimalToTwo("100.50")).toBe("100.50");
91+
});
92+
93+
it("rounds fractions longer than two digits", () => {
94+
expect(padDecimalToTwo("100.555")).toBe("100.56");
95+
});
96+
97+
it("returns non-numeric input unchanged", () => {
98+
expect(padDecimalToTwo("abc")).toBe("abc");
7299
});
73100
});

packages/app/src/modules/domain/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export {
4949
displayDecimal,
5050
displayInputDecimal,
5151
normalizeDecimalInput,
52+
padDecimalToTwo,
5253
parseNumber,
5354
} from "./shared/number";
5455
export type { CountyCode, RegionCode } from "./shared/regions";

packages/app/src/modules/domain/shared/number.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,17 @@ export function displayDecimal(value: string): string {
6363
: thousandFormatter.format(n);
6464
return decPart !== undefined ? `${formatted},${decPart}` : formatted;
6565
}
66+
67+
/**
68+
* Pad a stored decimal value to exactly two fraction digits.
69+
* Used on blur for euro inputs so every amount displays a consistent ",XX" suffix.
70+
*
71+
* Empty or non-numeric values pass through unchanged.
72+
* Example: `"100"` → `"100.00"`, `"100.5"` → `"100.50"`, `"100.555"` → `"100.56"`.
73+
*/
74+
export function padDecimalToTwo(value: string): string {
75+
if (!value) return value;
76+
const n = Number.parseFloat(value);
77+
if (Number.isNaN(n)) return value;
78+
return n.toFixed(2);
79+
}

0 commit comments

Comments
 (0)