Skip to content

Commit b27f11e

Browse files
committed
chore: refactor compliance forms error display
1 parent 4b3af20 commit b27f11e

9 files changed

Lines changed: 204 additions & 131 deletions

File tree

bciers/apps/compliance/src/app/components/compliance-summary/manage-obligation/apply-compliance-units/ApplyComplianceUnitsComponent.tsx

Lines changed: 75 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ import {
1616
ApplyComplianceUnitsFormData,
1717
BccrUnit,
1818
} from "@/compliance/src/app/types";
19-
import FormAlerts from "@bciers/components/form/FormAlerts";
2019
import { ApplyComplianceUnitsAlertNote } from "./ApplyComplianceUnitsAlertNote";
2120
import { IChangeEvent } from "@rjsf/core";
2221
import { actionHandler } from "@bciers/actions";
2322
import SubmitButton from "@bciers/components/button/SubmitButton";
2423
import getReportOperationByComplianceReportVersionId from "@/compliance/src/app/utils/getReportOperationByComplianceReportVersionId";
24+
import {
25+
useValidationErrors,
26+
handleApiResponse,
27+
} from "@bciers/components/validationErrors";
2528

2629
interface ApplyComplianceUnitsComponentProps {
2730
complianceReportVersionId: number;
@@ -44,14 +47,15 @@ export default function ApplyComplianceUnitsComponent({
4447
const [currentPhase, setCurrentPhase] = useState<
4548
"initial" | "confirmation" | "compliance_data"
4649
>("initial");
47-
const [errors, setErrors] = useState<string[] | undefined>();
4850
const [status, setStatus] = useState<Status>("idle");
4951
// Keep track of the remaining cap from the API (what's left to apply)
5052
const [remainingCap, setRemainingCap] = useState<number>(0);
5153
// Legacy / fallback outstanding balance (not used for limit enforcement)
5254
const [initialOutstandingBalance, setInitialOutstandingBalance] =
5355
useState<number>(0);
5456

57+
const { setErrors, renderedErrors } = useValidationErrors();
58+
5559
useEffect(() => {
5660
const fetchOperationName = async () => {
5761
const operation = await getReportOperationByComplianceReportVersionId(
@@ -107,6 +111,7 @@ export default function ApplyComplianceUnitsComponent({
107111
setFormData({
108112
bccr_holding_account_id: newAccountId,
109113
});
114+
setErrors(undefined);
110115
setStatus("idle");
111116
setCurrentPhase("initial");
112117
return;
@@ -162,51 +167,47 @@ export default function ApplyComplianceUnitsComponent({
162167
e: IChangeEvent<ApplyComplianceUnitsFormData>,
163168
) => {
164169
setStatus("submitting");
165-
try {
166-
const response = await actionHandler(
167-
`compliance/bccr/accounts/${e.formData?.bccr_holding_account_id}/compliance-report-versions/${complianceReportVersionId}/compliance-units`,
168-
"GET",
169-
"",
170-
);
170+
setErrors(undefined);
171171

172-
if (!response || response.error) {
173-
setStatus("idle");
174-
setErrors([response?.error || "Failed to get compliance units data."]);
175-
} else {
176-
// Set the remaining cap from the response (what’s left to apply)
177-
setRemainingCap(Number(response.compliance_unit_cap_remaining));
178-
179-
// Set the outstanding balance from the response
180-
setInitialOutstandingBalance(response.outstanding_balance || 0);
181-
182-
// Update form data with the full compliance data from the response
183-
setFormData((prev: Partial<ApplyComplianceUnitsFormData>) => {
184-
// Create a clean data object for the compliance phase
185-
const cleanFormData = {
186-
bccr_holding_account_id: prev.bccr_holding_account_id,
187-
bccr_trading_name: prev.bccr_trading_name,
188-
...response,
189-
};
190-
191-
setCurrentPhase("compliance_data");
192-
return cleanFormData;
193-
});
194-
195-
setStatus("submitted");
196-
setErrors(undefined);
197-
}
198-
} catch (err: any) {
199-
// Catch uncaught server errors
200-
setErrors([
201-
err?.message ||
202-
"An internal server error has occurred. Please contact support.",
203-
]);
172+
const response = await actionHandler(
173+
`compliance/bccr/accounts/${e.formData?.bccr_holding_account_id}/compliance-report-versions/${complianceReportVersionId}/compliance-units`,
174+
"GET",
175+
"",
176+
);
177+
178+
const isSuccess = handleApiResponse(response, setErrors);
179+
if (!isSuccess) {
180+
setStatus("idle");
181+
return;
204182
}
183+
184+
// Set the remaining cap from the response (what’s left to apply)
185+
setRemainingCap(Number(response.compliance_unit_cap_remaining));
186+
187+
// Set the outstanding balance from the response
188+
setInitialOutstandingBalance(response.outstanding_balance || 0);
189+
190+
// Update form data with the full compliance data from the response
191+
setFormData((prev: Partial<ApplyComplianceUnitsFormData>) => {
192+
// Create a clean data object for the compliance phase
193+
const cleanFormData = {
194+
bccr_holding_account_id: prev.bccr_holding_account_id,
195+
bccr_trading_name: prev.bccr_trading_name,
196+
...response,
197+
};
198+
199+
setCurrentPhase("compliance_data");
200+
return cleanFormData;
201+
});
202+
203+
setStatus("submitted");
205204
};
206205

207206
// Second submission: Apply the compliance units
208207
const handleApply = async () => {
209208
setStatus("applying");
209+
setErrors(undefined);
210+
210211
const response = await actionHandler(
211212
`compliance/bccr/accounts/${
212213
(formData as ApplyComplianceUnitsFormData)?.bccr_holding_account_id
@@ -217,13 +218,14 @@ export default function ApplyComplianceUnitsComponent({
217218
body: JSON.stringify(formData),
218219
},
219220
);
220-
if (!response || response.error) {
221+
222+
const isSuccess = handleApiResponse(response, setErrors);
223+
if (!isSuccess) {
221224
setStatus("submitted");
222-
setErrors([response.error || "Failed to apply compliance units."]);
223-
} else {
224-
setStatus("applied");
225-
setErrors(undefined);
225+
return;
226226
}
227+
228+
setStatus("applied");
227229
};
228230

229231
// Check if we should show the Submit button (when trading name is received)
@@ -318,11 +320,37 @@ export default function ApplyComplianceUnitsComponent({
318320
...response,
319321
}));
320322
setCurrentPhase("confirmation");
323+
setErrors(undefined);
321324
} else {
322325
setCurrentPhase("initial");
323326
}
324327
},
325-
onError: setErrors,
328+
onError: (errs?: any) => {
329+
if (!errs || (Array.isArray(errs) && errs.length === 0)) {
330+
setErrors(undefined);
331+
return;
332+
}
333+
const rawErrors = Array.isArray(errs) ? errs : [errs];
334+
const formatted = rawErrors.map((err) => {
335+
if (typeof err === "string") {
336+
return {
337+
key: err,
338+
error: { severity: "Error", message: err },
339+
};
340+
}
341+
if (err && !err.error) {
342+
return {
343+
key: err.key || "error",
344+
error: {
345+
severity: "Error",
346+
message: err.message || String(err),
347+
},
348+
};
349+
}
350+
return err;
351+
});
352+
setErrors(formatted as any);
353+
},
326354
complianceLimitStatus,
327355
isApplied: status === "applied",
328356
maxCreditUsagePercentage: (formData as ApplyComplianceUnitsFormData)
@@ -336,7 +364,7 @@ export default function ApplyComplianceUnitsComponent({
336364
<ApplyComplianceUnitsAlertNote />
337365
</div>
338366
)}
339-
<FormAlerts errors={errors} />
367+
{renderedErrors}
340368
<ComplianceStepButtons
341369
backButtonText={status === "applied" ? "Back" : "Cancel"}
342370
onBackClick={() =>

bciers/apps/compliance/src/app/components/compliance-summary/manage-obligation/automatic-overdue-penalty/review-penalty-summary/PenaltySummaryReviewComponent.tsx

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import { AutomaticOverduePenalty } from "@/compliance/src/app/types";
1010
import { useState } from "react";
1111
import generateInvoice from "@/compliance/src/app/utils/generateInvoice";
1212
import { ComplianceInvoiceTypes } from "@bciers/utils/src/enums";
13-
import FormAlerts from "@bciers/components/form/FormAlerts";
13+
import {
14+
useValidationErrors,
15+
handleApiResponse,
16+
} from "@bciers/components/validationErrors";
1417

1518
interface Props {
1619
data: AutomaticOverduePenalty;
@@ -35,7 +38,9 @@ const PenaltySummaryReviewComponent = ({
3538
}`;
3639
const saveAndContinueUrl = `/compliance-administration/compliance-summaries/${complianceReportVersionId}/download-payment-penalty-instructions`;
3740

38-
const [errors, setErrors] = useState<string[]>([]);
41+
const { setErrors, renderedErrors } = useValidationErrors({
42+
config: {},
43+
});
3944
const [isGeneratingPenaltyInvoice, setIsGeneratingPenaltyInvoice] =
4045
useState(false);
4146

@@ -45,20 +50,16 @@ const PenaltySummaryReviewComponent = ({
4550
const formData = { ...data, penalty_status: displayPenaltyStatus };
4651

4752
const handleGeneratePenaltyInvoice = async () => {
48-
setErrors([]);
53+
setErrors(undefined);
4954
setIsGeneratingPenaltyInvoice(true);
5055

51-
try {
52-
await generateInvoice(
53-
complianceReportVersionId,
54-
ComplianceInvoiceTypes.AUTOMATIC_OVERDUE_PENALTY,
55-
);
56-
} catch (err) {
57-
const msg = err instanceof Error ? err.message : String(err);
58-
setErrors([msg]);
59-
} finally {
60-
setIsGeneratingPenaltyInvoice(false);
61-
}
56+
const response = await generateInvoice(
57+
complianceReportVersionId,
58+
ComplianceInvoiceTypes.AUTOMATIC_OVERDUE_PENALTY,
59+
);
60+
61+
handleApiResponse(response, setErrors);
62+
setIsGeneratingPenaltyInvoice(false);
6263
};
6364

6465
return (
@@ -69,7 +70,7 @@ const PenaltySummaryReviewComponent = ({
6970
formData={formData}
7071
className="w-full"
7172
>
72-
<FormAlerts key="alerts" errors={errors} />
73+
{renderedErrors}
7374
<ComplianceStepButtons
7475
backUrl={backUrl}
7576
continueUrl={saveAndContinueUrl}

bciers/apps/compliance/src/app/components/compliance-summary/manage-obligation/download-payment-instructions/PaymentInstructionsDownloadComponent.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import {
77
createDownloadPaymentInstructionsSchema,
88
downloadPaymentInstructionsUiSchema,
99
} from "@/compliance/src/app/data/jsonSchema/manageObligation/downloadPaymentInstructionsSchema";
10-
import FormAlerts from "@bciers/components/form/FormAlerts";
1110
import { ComplianceInvoiceTypes } from "@bciers/utils/src/enums";
1211
import buildQueryParams from "@bciers/utils/src/buildQueryParams";
12+
import {
13+
useValidationErrors,
14+
createGenericValidationError,
15+
} from "@bciers/components/validationErrors";
1316

1417
interface Props {
1518
readonly complianceReportVersionId: number;
@@ -32,7 +35,10 @@ export default function PaymentInstructionsDownloadComponent({
3235
const saveAndContinueUrl =
3336
customContinueUrl ??
3437
`/compliance-administration/compliance-summaries/${complianceReportVersionId}/pay-obligation-track-payments`;
35-
const [errors, setErrors] = useState<string[]>([]);
38+
39+
const { setErrors, renderedErrors } = useValidationErrors({
40+
config: {},
41+
});
3642
const [isGeneratingDownload, setIsGeneratingDownload] = useState(false);
3743
const instructionFormData = {
3844
complianceReportVersionId,
@@ -46,9 +52,13 @@ export default function PaymentInstructionsDownloadComponent({
4652
bank_address: "1175 DOUGLAS STREET, VICTORIA, BC V8W2E1",
4753
};
4854

55+
const setError = (message: string) => {
56+
setErrors([createGenericValidationError(message)]);
57+
};
58+
4959
// Borrowed logic from complianceSummaryReviewComponent
5060
const handleDownloadInstructions = async () => {
51-
setErrors([]);
61+
setErrors(undefined);
5262
setIsGeneratingDownload(true);
5363

5464
try {
@@ -78,22 +88,22 @@ export default function PaymentInstructionsDownloadComponent({
7888
}
7989

8090
if (typeof payload.message === "string") {
81-
setErrors([payload.message]);
91+
setError(payload.message);
8292
return;
8393
}
8494

8595
// Generic fallback message
86-
setErrors([
96+
setError(
8797
`Failed to generate payment instructions (status ${res.status})`,
88-
]);
98+
);
8999
return;
90100
}
91101

92102
// Handle non-JSON response errors
93103
if (!res.ok) {
94-
setErrors([
104+
setError(
95105
`Failed to generate payment instructions (status ${res.status})`,
96-
]);
106+
);
97107
return;
98108
}
99109

@@ -106,11 +116,12 @@ export default function PaymentInstructionsDownloadComponent({
106116
setTimeout(() => URL.revokeObjectURL(objectUrl), 30_000);
107117
} catch (err) {
108118
const msg = err instanceof Error ? err.message : String(err);
109-
setErrors([msg]);
119+
setError(msg);
110120
} finally {
111121
setIsGeneratingDownload(false);
112122
}
113123
};
124+
114125
return (
115126
<FormBase
116127
schema={createDownloadPaymentInstructionsSchema()}
@@ -124,7 +135,7 @@ export default function PaymentInstructionsDownloadComponent({
124135
].includes(invoiceType as ComplianceInvoiceTypes),
125136
}}
126137
>
127-
<FormAlerts key="alerts" errors={errors} />
138+
{renderedErrors}
128139
<ComplianceStepButtons
129140
key="form-buttons"
130141
backUrl={backUrl}

0 commit comments

Comments
 (0)