Skip to content

Commit e2dd771

Browse files
authored
COMP-731 warning before locking internally tracked enforcements (#644)
1 parent c1bc7dc commit e2dd771

File tree

5 files changed

+145
-30
lines changed

5 files changed

+145
-30
lines changed

compliance-web/src/components/App/Inspections/Profile/Enforcements/AdministrativePenalty/AdministrativePenaltyUpdateModal.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ const AdministrativePenaltyUpdateModal: FC<
129129

130130
const { reset, handleSubmit, watch } = methods;
131131
const decision = watch("decision");
132+
const referralStatus = watch("referral_status");
132133

133134
useEffect(() => {
134135
reset(defaultValues);
@@ -146,6 +147,17 @@ const AdministrativePenaltyUpdateModal: FC<
146147
}
147148
}, [decision, methods]);
148149

150+
// Determine if save confirmation is required
151+
const requireSaveConfirmation = useMemo(() => {
152+
const isCEBNotProceeding =
153+
referralStatus?.id === APReferralStatus.CEB_NOT_PROCEEDING.id;
154+
const isReferredToDMWithDecision =
155+
referralStatus?.id === APReferralStatus.REFERRED_TO_DM.id &&
156+
decision !== null;
157+
158+
return isCEBNotProceeding || isReferredToDMWithDecision;
159+
}, [referralStatus, decision]);
160+
149161
const onUpdateSuccess = (data: AdministrativePenalty) => {
150162
// Invalidate all administrative penalties because of the AP can be linked to
151163
// other inspections
@@ -185,7 +197,7 @@ const AdministrativePenaltyUpdateModal: FC<
185197
const isLinkedToOtherInspections = useMemo(()=>{
186198
const isParent = administrativePenalty.inspection_id == inspectionData.id;
187199
return (linkedData?.some(
188-
(linkData) => linkData.inspection.id !== inspectionData.id
200+
(linkData) => linkData.inspection.id !== inspectionData.id
189201
) ?? false) && isParent;
190202
}, [linkedData, inspectionData, administrativePenalty]);
191203

@@ -323,6 +335,7 @@ const AdministrativePenaltyUpdateModal: FC<
323335
? "This AP is linked to other files. Deleting it will remove all linked APs."
324336
: undefined
325337
}
338+
requireSaveConfirmation={requireSaveConfirmation}
326339
/>
327340
)}
328341
</form>

compliance-web/src/components/App/Inspections/Profile/Enforcements/ChargeRecommendation/ChargeRecommendationUpdateModal.tsx

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
import { Inspection } from "@/models/Inspection";
2323
import { notify } from "@/store/snackbarStore";
2424
import { useModal } from "@/store/modalStore";
25-
import { CRStatus, CRDecision, CRCourtDecision } from "@/utils/constants";
25+
import { CRStatus, CRDecision, CRCourtDecision, ChargeRecommendationStatus } from "@/utils/constants";
2626
import dayjs, { Dayjs } from "dayjs";
2727
import { KC_USER_GROUPS, useIsRolesAllowed } from "@/hooks/useAuthorization";
2828

@@ -75,8 +75,8 @@ const decisionOptions: DecisionOption[] = Object.values(CRDecision).map(
7575

7676
const courtDecisionOptions: CourtDecisionOption[] = Object.values(CRCourtDecision).map(
7777
(courtDecision) => ({
78-
id: courtDecision.id,
79-
name: courtDecision.name,
78+
id: courtDecision.id,
79+
name: courtDecision.name,
8080
})
8181
);
8282

@@ -120,9 +120,9 @@ const ChargeRecommendationUpdateModal: FC<
120120

121121
// Convert sentence type mappings to sentence type options for form
122122
const selectedSentenceTypes = chargeRecommendationData.sentence_type_mappings?.map(mapping => ({
123-
id: mapping.sentence_type_option_id,
124-
name: mapping.sentence_type_option.name,
125-
})) || [];
123+
id: mapping.sentence_type_option_id,
124+
name: mapping.sentence_type_option.name,
125+
})) || [];
126126

127127
return {
128128
status: selectedStatusOption,
@@ -152,6 +152,22 @@ const ChargeRecommendationUpdateModal: FC<
152152
defaultValues,
153153
});
154154

155+
const { watch } = methods;
156+
const status = watch("status");
157+
const chargeDecision = watch("charge_decision");
158+
const sentenceDate = watch("sentence_date");
159+
160+
const requireSaveConfirmation = useMemo(() => {
161+
const isCEBNotProceeding = status?.id === ChargeRecommendationStatus.CEB_NOT_PROCEEDING;
162+
const isChargeDecisionNotProceeding =
163+
chargeDecision?.id === CRDecision.NOT_PROCEEDING.id;
164+
const hasSentenceDate = sentenceDate !== null && sentenceDate !== undefined;
165+
166+
return (
167+
isCEBNotProceeding || isChargeDecisionNotProceeding || hasSentenceDate
168+
);
169+
}, [status, chargeDecision, sentenceDate]);
170+
155171
const onUpdateSuccess = (
156172
updatedChargeRecommendation: ChargeRecommendation
157173
) => {
@@ -330,25 +346,25 @@ const ChargeRecommendationUpdateModal: FC<
330346
/>
331347
</Box>
332348
<Box sx={{ display: "flex", gap: 2 }}>
333-
<ControlledAutoComplete
334-
name="sentence_types"
335-
label="Sentence Type"
336-
options={sentenceTypeOptions}
337-
getOptionLabel={(option: SentenceTypeOption) => option.name}
338-
getOptionKey={(option: SentenceTypeOption) => option.id}
349+
<ControlledAutoComplete
350+
name="sentence_types"
351+
label="Sentence Type"
352+
options={sentenceTypeOptions}
353+
getOptionLabel={(option: SentenceTypeOption) => option.name}
354+
getOptionKey={(option: SentenceTypeOption) => option.id}
339355
isOptionEqualToValue={(option: SentenceTypeOption, value: SentenceTypeOption) =>
340356
option.id.toString() === value.id.toString()
341357
}
342-
multiple
343-
fullWidth
344-
disabled={isReadonlyMode || isSentenceTypesLoading}
345-
loading={isSentenceTypesLoading}
346-
/>
358+
multiple
359+
fullWidth
360+
disabled={isReadonlyMode || isSentenceTypesLoading}
361+
loading={isSentenceTypesLoading}
362+
/>
347363
<ControlledDateField name="sentence_date" label="Sentence Date" disabled={isReadonlyMode} />
348364
</Box>
349-
<ControlledTextField
350-
name="sentence_description"
351-
label="Sentence Description"
365+
<ControlledTextField
366+
name="sentence_description"
367+
label="Sentence Description"
352368
disabled={isReadonlyMode}
353369
multiline
354370
rows={2}
@@ -364,6 +380,7 @@ const ChargeRecommendationUpdateModal: FC<
364380
secondaryActionButtonText="Cancel"
365381
onDeleteAction={handleDelete}
366382
isDeleteActionLoading={isDeleting}
383+
requireSaveConfirmation={requireSaveConfirmation}
367384
/>
368385
)}
369386
</form>

compliance-web/src/components/App/Inspections/Profile/Enforcements/RestorativeJustice/RestorativeJusticeUpdateModal.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,15 @@ const RestorativeJusticeUpdateModal: FC<RestorativeJusticeUpdateModalProps> = ({
6767
defaultValues,
6868
});
6969

70-
const { reset, handleSubmit } = methods;
70+
const { reset, handleSubmit, watch } = methods;
71+
const dateRestitutionComplete = watch("date_restitution_complete");
72+
73+
// Determine if save confirmation is required
74+
const requireSaveConfirmation = useMemo(() => {
75+
return (
76+
dateRestitutionComplete !== null && dateRestitutionComplete !== undefined
77+
);
78+
}, [dateRestitutionComplete]);
7179

7280
useEffect(() => {
7381
reset(defaultValues);
@@ -153,6 +161,7 @@ const RestorativeJusticeUpdateModal: FC<RestorativeJusticeUpdateModalProps> = ({
153161
onDeleteAction={() => {
154162
deleteRestorativeJustice({ restorativeJusticeId: restorativeJustice.id });
155163
}}
164+
requireSaveConfirmation={requireSaveConfirmation}
156165
/>
157166
)}
158167
</form>

compliance-web/src/components/App/Inspections/Profile/Enforcements/ViolationTicket/ViolationTicketUpdateModal.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const violationTicketUpdateSchema = yup.object().shape({
2626
ticket_number: yup.string().required("Ticket Number is required"),
2727
date_issued: yup.mixed<Dayjs>().required("Date Issued is required").typeError("Invalid date"),
2828
fine_amount: yup.number().transform((value, originalValue) => {
29-
return originalValue === "" ? null : value;
29+
return originalValue === "" ? null : value;
3030
}).required("Fine Amount is required").min(0, "Fine Amount must be positive"),
3131
status: yup.mixed<StatusOption>().required("Status is required"),
3232
status_date: yup.mixed<Dayjs>().required("Status Date is required").typeError("Invalid date"),
@@ -91,12 +91,26 @@ const ViolationTicketUpdateModal: FC<ViolationTicketUpdateModalProps> = ({
9191
defaultValues,
9292
});
9393

94-
const { reset, handleSubmit } = methods;
94+
const { reset, handleSubmit, watch } = methods;
95+
const status = watch("status");
96+
const statusDate = watch("status_date");
9597

9698
useEffect(() => {
9799
reset(defaultValues);
98100
}, [reset, defaultValues]);
99101

102+
// Determine if save confirmation is required
103+
const requireSaveConfirmation = useMemo(() => {
104+
const isPaidWithStatusDate =
105+
status?.id === ViolationTicketStatus.PAID &&
106+
statusDate !== null &&
107+
statusDate !== undefined;
108+
const isDisputed = status?.id === ViolationTicketStatus.DISPUTED;
109+
const isDeemedGuilty = status?.id === ViolationTicketStatus.DEEMED_GUILTY;
110+
111+
return isPaidWithStatusDate || isDisputed || isDeemedGuilty;
112+
}, [status, statusDate]);
113+
100114
const onUpdateSuccess = (data: ViolationTicket) => {
101115
queryClient.invalidateQueries({
102116
queryKey: ["inspection-violation-tickets", inspectionData.id],
@@ -126,8 +140,8 @@ const ViolationTicketUpdateModal: FC<ViolationTicketUpdateModalProps> = ({
126140
const updateData: ViolationTicketAPIData = {
127141
inspection_id: inspectionData?.id ?? 0,
128142
inspection_requirement_ids: violationTicket.violation_ticket_requirement_maps.map(
129-
(map) => map.inspection_requirement_id
130-
),
143+
(map) => map.inspection_requirement_id
144+
),
131145
ticket_number: data.ticket_number,
132146
date_issued: data.date_issued.format("YYYY-MM-DDTHH:mm:ss.SSS[Z]"),
133147
fine_amount: data.fine_amount?.toString() || "",
@@ -187,8 +201,8 @@ const ViolationTicketUpdateModal: FC<ViolationTicketUpdateModalProps> = ({
187201
}}
188202
InputProps={{
189203
startAdornment: <AttachMoneyRounded sx={{
190-
mr: 0.2,
191-
color: "#9F9D9C",
204+
mr: 0.2,
205+
color: "#9F9D9C",
192206
}} />,
193207
}}
194208
isRequired={true}
@@ -225,6 +239,7 @@ const ViolationTicketUpdateModal: FC<ViolationTicketUpdateModalProps> = ({
225239
secondaryActionButtonText="Cancel"
226240
onDeleteAction={handleDelete}
227241
isDeleteActionLoading={isPendingDelete}
242+
requireSaveConfirmation={requireSaveConfirmation}
228243
/>
229244
)}
230245
</form>

compliance-web/src/components/Shared/Modals/ModalActions.tsx

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ type ModalActionsProps = {
1717
isLoading?: boolean;
1818
isDeleteActionLoading?: boolean;
1919
hideSecondaryButton?: boolean;
20+
requireSaveConfirmation?: boolean;
21+
onSaveConfirmationText?: string;
22+
onSaveConfirmationTitle?: string;
2023
};
2124

2225
const ModalActions: FC<ModalActionsProps> = ({
@@ -30,16 +33,28 @@ const ModalActions: FC<ModalActionsProps> = ({
3033
isLoading = false,
3134
isDeleteActionLoading = false,
3235
hideSecondaryButton = false,
36+
requireSaveConfirmation = false,
37+
onSaveConfirmationText = "All required information has been entered, so please review the details before continuing.",
38+
onSaveConfirmationTitle = "Locking Enforcement",
3339
}) => {
3440
const { setClose } = useModal();
3541
const formContext = useFormContext();
3642
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
43+
const [showSaveConfirmation, setShowSaveConfirmation] = useState(false);
3744

3845
const isValid = isButtonValidation ? formContext?.formState.isValid : true;
3946

47+
const handlePrimaryAction = () => {
48+
if (requireSaveConfirmation && !showSaveConfirmation) {
49+
setShowSaveConfirmation(true);
50+
} else {
51+
onPrimaryAction?.();
52+
}
53+
};
54+
4055
return (
4156
<>
42-
{!showDeleteConfirmation && (
57+
{!showDeleteConfirmation && !showSaveConfirmation && (
4358
<DialogActions
4459
sx={{
4560
padding: "1rem 1.5rem",
@@ -77,7 +92,7 @@ const ModalActions: FC<ModalActionsProps> = ({
7792
sx={{ minWidth: 100 }}
7893
type={onPrimaryAction ? "button" : "submit"}
7994
isLoading={isLoading}
80-
onClick={onPrimaryAction}
95+
onClick={handlePrimaryAction}
8196
disabled={
8297
(!!isButtonValidation && !isValid) || showDeleteConfirmation
8398
}
@@ -134,6 +149,52 @@ const ModalActions: FC<ModalActionsProps> = ({
134149
</LoadingButton>
135150
</Box>
136151
)}
152+
{showSaveConfirmation && (
153+
<Box
154+
sx={{
155+
background: BCDesignTokens.supportSurfaceColorDanger,
156+
borderTop: `${BCDesignTokens.layoutBorderWidthSmall} solid ${BCDesignTokens.surfaceColorBorderDefault}`,
157+
padding: ".5rem 1.5rem",
158+
display: "flex",
159+
justifyContent: "space-between",
160+
alignItems: "center",
161+
gap: "0.5rem",
162+
}}
163+
>
164+
<Box
165+
sx={{
166+
display: "flex",
167+
flexDirection: "column",
168+
flexWrap: "wrap",
169+
flex: 1,
170+
}}
171+
>
172+
<Typography variant="body1" fontWeight={"bold"}>
173+
{onSaveConfirmationTitle}
174+
</Typography>
175+
<Typography variant="body1">{onSaveConfirmationText}</Typography>
176+
</Box>
177+
<Button
178+
sx={{ minWidth: 100, height: 40 }}
179+
color="secondary"
180+
onClick={() => {
181+
setShowSaveConfirmation(false);
182+
}}
183+
data-testid="save-confirmation-cancel-button"
184+
>
185+
No, Cancel
186+
</Button>
187+
<LoadingButton
188+
sx={{ minWidth: 100, height: 40 }}
189+
onClick={onPrimaryAction}
190+
color="error"
191+
data-testid="save-confirmation-button"
192+
isLoading={isLoading}
193+
>
194+
Yes, Save
195+
</LoadingButton>
196+
</Box>
197+
)}
137198
</>
138199
);
139200
};

0 commit comments

Comments
 (0)