From 1714da6e37eafb6c140c1368caa44540d2df4c2a Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Tue, 25 Aug 2026 18:19:06 -0300 Subject: [PATCH 1/4] fix: send customer on board work-order create Org-wide Admin create needs exact Accounts.Name so SH-221 can stamp AccountId. --- .../table/build-inline-draft-create-input.ts | 1 + .../table/cells/build-site-dialog-patch.ts | 3 ++ .../list/table/cells/site-cell.tsx | 3 ++ .../list/table/cells/site-dialog-types.ts | 1 + .../list/table/cells/site-dialog.tsx | 11 +++++ .../list/table/cells/use-site-dialog-state.ts | 27 +++++++++- .../table/wo-table-inline-identity-cells.tsx | 2 + .../list/table/wo-table-inline-row.tsx | 4 +- .../wizard/wizard-sidebar-summary-rows.ts | 1 + .../wizard-step-location-account-select.tsx | 49 +++++++++++++++++++ .../wizard/wizard-step-location-service.tsx | 6 +++ .../mappers/work-order-board-mapper.ts | 5 ++ .../work-orders/types/work-order-board.ts | 2 + .../work-orders/types/work-order-table-row.ts | 3 ++ .../work-orders/types/work-order-wizard.ts | 5 +- .../utils/get-inline-create-missing.ts | 10 +++- .../build-inline-draft-create-input.test.ts | 3 ++ ...zard-step-location-account-select.test.tsx | 48 ++++++++++++++++++ .../work-orders/api/work-orders-api.test.ts | 2 + .../mappers/wizard-draft-mapper.test.ts | 20 +++++++- .../mappers/work-order-board-mapper.test.ts | 22 +++++++++ .../utils/get-inline-create-missing.test.ts | 19 +++++-- 22 files changed, 237 insertions(+), 10 deletions(-) create mode 100644 src/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select.tsx create mode 100644 src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx diff --git a/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts b/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts index 5f911bd8..169f062e 100644 --- a/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts +++ b/src/app/(protected)/workorders/_components/list/table/build-inline-draft-create-input.ts @@ -19,6 +19,7 @@ export function buildInlineDraftCreateInput( locationId: String(draft.locationId || ""), locationName, siteCode: location ? resolveSiteCode(location) : resolveSiteCode({ name: locationName }), + customer: draft.customer.trim(), woNumber: draft.woNumber, type: draft.type || "PM", severity: draft.severity, diff --git a/src/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch.ts b/src/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch.ts index 3fe930cd..1b9e23c3 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch.ts +++ b/src/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch.ts @@ -8,6 +8,7 @@ type BuildSiteDialogPatchArgs = { code: string; locId: string; selected: LocationOption | undefined; + customer?: string; pocName: string; pocPhone: string; pocNotes: string; @@ -20,6 +21,7 @@ export function buildSiteDialogPatch({ code, locId, selected, + customer, pocName, pocPhone, pocNotes, @@ -31,6 +33,7 @@ export function buildSiteDialogPatch({ site: code || (selected ? resolveSiteCode(selected) : ""), location: selected ? extractLocationLabel(selected) : "", locationId: locId, + ...(customer !== undefined ? { customer } : {}), pocName, pocPhone, pocNotes, diff --git a/src/app/(protected)/workorders/_components/list/table/cells/site-cell.tsx b/src/app/(protected)/workorders/_components/list/table/cells/site-cell.tsx index 4c40ae06..04077833 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/site-cell.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/site-cell.tsx @@ -11,6 +11,7 @@ type SiteCellProps = { value: string; location: string; locationId: string | number; + customer?: string; city?: string; state?: string; pocName?: string; @@ -29,6 +30,7 @@ export function SiteCell({ value, location, locationId, + customer, city, state, pocName, @@ -95,6 +97,7 @@ export function SiteCell({ onOpenChange={setOpen} value={value} locationId={locationId} + customer={customer} pocName={pocName} pocPhone={pocPhone} pocNotes={pocNotes} diff --git a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-types.ts b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-types.ts index 499af6aa..956d54ba 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-types.ts +++ b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-types.ts @@ -4,6 +4,7 @@ export type SitePatch = { site: string; location: string; locationId: string | number; + customer?: string; pocName: string; pocPhone: string; pocNotes: string; diff --git a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx index 0843e331..ecf6e3ba 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx @@ -9,6 +9,7 @@ import type { LocationOption } from "@/domain/work-orders/types/work-order"; import { FieldLabel } from "@/app/(protected)/workorders/_components/list/table/cells/dialog-field-label"; import { SiteDialogDetailPanel } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-detail-panel"; import { SiteDialogFooter } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-footer"; +import { WizardStepLocationAccountSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select"; import { SiteDialogPocSection } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-poc-section"; import type { SitePatch } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-types"; import { useSiteDialogState } from "@/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state"; @@ -20,6 +21,7 @@ type SiteDialogProps = { onOpenChange: (open: boolean) => void; value: string; locationId: string | number; + customer?: string; pocName?: string; pocPhone?: string; pocNotes?: string; @@ -35,6 +37,7 @@ export function SiteDialog({ onOpenChange, value, locationId, + customer = "", pocName = "", pocPhone = "", pocNotes = "", @@ -49,6 +52,7 @@ export function SiteDialog({ onOpenChange, value, locationId, + customer, pocName, pocPhone, pocNotes, @@ -81,6 +85,13 @@ export function SiteDialog({
+ {createMode && ( + + )}
Site void; value: string; locationId: string | number; + customer?: string; pocName: string; pocPhone: string; pocNotes: string; @@ -29,6 +30,7 @@ export function useSiteDialogState({ onOpenChange, value, locationId, + customer = "", pocName, pocPhone, pocNotes, @@ -38,6 +40,7 @@ export function useSiteDialogState({ onSave, }: UseSiteDialogStateArgs) { const [code, setCode] = useState(value); + const [accountName, setAccountName] = useState(customer); const [locId, setLocId] = useState(() => resolveLocationId(locationId, value, sites)); const [pn, setPn] = useState(pocName); const [pp, setPp] = useState(pocPhone); @@ -60,8 +63,9 @@ export function useSiteDialogState({ const selected = sites.find((s) => String(s.id) === locId); const addressPreview = locationDetail ? formatLocationAddressPreview(locationDetail) : ""; const siteMissing = !locId; + const accountMissing = createMode && !accountName.trim(); const pocMissing = !pn.trim() || !pp.trim(); - const canConfirm = !siteMissing && (!createMode || !pocMissing); + const canConfirm = !siteMissing && (!createMode || (!pocMissing && !accountMissing)); useEffect(() => { if (!open) { @@ -77,6 +81,7 @@ export function useSiteDialogState({ } const resolved = resolveLocationId(locationId, value, sites); setCode(value); + setAccountName(customer); setLocId(resolved); setPn(pocName); setPp(pocPhone); @@ -84,7 +89,18 @@ export function useSiteDialogState({ setExtraContacts(additionalContacts); setBaselineHadContacts(additionalContacts.length > 0); setPocFilledFor(pocName.trim() || pocPhone.trim() ? resolved : ""); - }, [open, dirty, value, locationId, pocName, pocPhone, pocNotes, additionalContacts, sites]); + }, [ + open, + dirty, + value, + locationId, + customer, + pocName, + pocPhone, + pocNotes, + additionalContacts, + sites, + ]); useEffect(() => { if (!open || !locId || !locationDetail || pocFilledFor === locId) { @@ -121,6 +137,7 @@ export function useSiteDialogState({ code, locId, selected, + ...(createMode ? { customer: accountName } : {}), pocName: pn, pocPhone: pp, pocNotes: notes, @@ -152,6 +169,8 @@ export function useSiteDialogState({ return { locId, code, + accountName, + accountMissing, selected, locationDetail, locationDetailLoading, @@ -164,6 +183,10 @@ export function useSiteDialogState({ showErrors, siteMissing, handlePick, + setAccountName: (next: string) => { + setDirty(true); + setAccountName(next); + }, attemptSave, setPn: (next: string) => markPocDirty(() => setPn(next)), setPp: (next: string) => markPocDirty(() => setPp(next)), diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-identity-cells.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-identity-cells.tsx index 47ca7b46..f650a040 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-identity-cells.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-identity-cells.tsx @@ -50,6 +50,7 @@ export function WoTableInlineIdentityCells({ value={draft.site} location={draft.location} locationId={draft.locationId} + customer={draft.customer} city={sitePlace?.city} state={sitePlace?.state} pocName={draft.pocName} @@ -63,6 +64,7 @@ export function WoTableInlineIdentityCells({ site: p.site, location: p.location, locationId: p.locationId, + ...(p.customer !== undefined ? { customer: p.customer } : {}), pocName: p.pocName, pocPhone: p.pocPhone, pocNotes: p.pocNotes, diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx index 0e29c529..94431d21 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx @@ -78,7 +78,9 @@ export function WoTableInlineRow({ const siteCellRef = useRef(null); const woCellRef = useRef(null); - const siteCellMissing = missing.some((f) => f === "site" || f === "pocName" || f === "pocPhone"); + const siteCellMissing = missing.some( + (f) => f === "customer" || f === "site" || f === "pocName" || f === "pocPhone", + ); const siteShowError = showErrors && siteCellMissing; const woShowError = showErrors && missing.includes("woNumber"); diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary-rows.ts b/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary-rows.ts index e49e267a..3cd23c7d 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary-rows.ts +++ b/src/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary-rows.ts @@ -46,6 +46,7 @@ export function buildWizardSidebarSummaryRows(draft: WorkOrderWizardDraft): [str ["Schedule on", scheduleOnLabel(draft)], ["Due date", fmtDate(draft.dueDate) || "—"], ["Assigned to", draft.assigneeName || "—"], + ["Account", draft.customer.trim() || "—"], ["Site", draft.siteCode || draft.locationName || "—"], ["POC", pocLabel(draft)], ["Additional POCs", formatAdditionalPocSummary(draft)], diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select.tsx new file mode 100644 index 00000000..5ab5a3e1 --- /dev/null +++ b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select.tsx @@ -0,0 +1,49 @@ +import { Building2, Info } from "lucide-react"; +import { Text } from "@/components/ui/text"; +import { WizardFieldSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-field-select"; +import { WizLabel } from "@/app/(protected)/workorders/_components/wizard/wizard-labels"; +import { useAccountsList } from "@/domain/accounts/use-cases/use-accounts-list"; + +type WizardStepLocationAccountSelectProps = { + customer: string; + onCustomerChange: (customer: string) => void; + showError?: boolean; +}; + +export function WizardStepLocationAccountSelect({ + customer, + onCustomerChange, + showError = false, +}: WizardStepLocationAccountSelectProps) { + const { data } = useAccountsList({ page: 1, pageSize: 100, search: "" }); + const accounts = data?.items ?? []; + const options = accounts + .map((account) => account.name.trim()) + .filter(Boolean) + .map((name) => ({ key: name, label: name })); + + return ( +
+ Account + + } + searchPlaceholder="Search account name…" + options={options} + onPick={onCustomerChange} + /> + + Account is required to create a work order. + +
+ ); +} diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service.tsx index 950ca5c8..17425052 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-service.tsx @@ -2,6 +2,7 @@ import { Text } from "@/components/ui/text"; import type { LocationOption } from "@/domain/work-orders/types/work-order"; import type { Location } from "@/domain/locations/types/location"; import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard"; +import { WizardStepLocationAccountSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select"; import { WizardStepLocationPocFields } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-poc-fields"; import { WizardStepLocationServiceSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-service-select"; import { WizardStepLocationSiteDetails } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-site-details"; @@ -44,6 +45,11 @@ export function WizardStepLocationService({ return (
+ onPatch({ customer })} + /> = { + customer: "Account", site: "Site", woNumber: "WO #", pocName: "POC name", @@ -9,6 +10,7 @@ export const INLINE_CREATE_REQ_LABELS: Record = { }; export type InlineCreateDraftFields = { + customer?: string; site?: string; locationId?: string | number; woNumber?: string; @@ -18,6 +20,9 @@ export type InlineCreateDraftFields = { export function getInlineCreateMissing(d: InlineCreateDraftFields): InlineCreateReqField[] { const miss: InlineCreateReqField[] = []; + if (!d.customer?.trim()) { + miss.push("customer"); + } const hasSite = Boolean(d.site?.trim()) || Boolean(String(d.locationId ?? "").trim()); if (!hasSite) { miss.push("site"); @@ -43,6 +48,9 @@ export function inlineCreateSaveHint(miss: InlineCreateReqField[]): string { } export function inlineCreateSiteError(miss: InlineCreateReqField[]): string { + if (miss.includes("customer")) { + return "Account is required"; + } if (miss.includes("site")) { return "Site is required"; } diff --git a/src/test/app/(protected)/workorders/build-inline-draft-create-input.test.ts b/src/test/app/(protected)/workorders/build-inline-draft-create-input.test.ts index ccccd7d3..16b30ee8 100644 --- a/src/test/app/(protected)/workorders/build-inline-draft-create-input.test.ts +++ b/src/test/app/(protected)/workorders/build-inline-draft-create-input.test.ts @@ -8,6 +8,7 @@ describe("buildInlineDraftCreateInput", () => { const draft = { ...EMPTY_TABLE_DRAFT, site: "BK5", + customer: "Amazon", locationId: "9", woNumber: "25001", pocName: "Jane", @@ -30,6 +31,8 @@ describe("buildInlineDraftCreateInput", () => { expect(payload.scheduleWeekOnly).toBeUndefined(); expect(payload.woNumber).toBe("25001"); expect(payload.pocName).toBe("Jane"); + expect(payload.customer).toBe("Amazon"); + expect(input.customer).toBe("Amazon"); }); it("keeps week-only schedule when targetWeek is set", () => { diff --git a/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx b/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx new file mode 100644 index 00000000..d0bc6a2c --- /dev/null +++ b/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx @@ -0,0 +1,48 @@ +import { fireEvent, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { WizardStepLocationAccountSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select"; +import { renderWithProviders } from "@/test/test-utils"; + +vi.mock("@/domain/accounts/use-cases/use-accounts-list", () => ({ + useAccountsList: () => ({ + data: { + items: [ + { + id: 1, + name: "Amazon", + number: "", + industry: "", + website: "", + primaryContact: "", + phoneNumber: "", + owner: "", + }, + { + id: 2, + name: "Sea Haven", + number: "", + industry: "", + website: "", + primaryContact: "", + phoneNumber: "", + owner: "", + }, + ], + }, + }), +})); + +describe("WizardStepLocationAccountSelect", () => { + it("sets draft.customer to the exact account name on pick", () => { + const onCustomerChange = vi.fn(); + renderWithProviders( + , + { withAuth: false }, + ); + + fireEvent.click(screen.getByRole("button", { name: /select account/i })); + fireEvent.click(screen.getByRole("button", { name: "Amazon" })); + + expect(onCustomerChange).toHaveBeenCalledWith("Amazon"); + }); +}); diff --git a/src/test/domain/work-orders/api/work-orders-api.test.ts b/src/test/domain/work-orders/api/work-orders-api.test.ts index 388ad617..fabf80af 100644 --- a/src/test/domain/work-orders/api/work-orders-api.test.ts +++ b/src/test/domain/work-orders/api/work-orders-api.test.ts @@ -114,6 +114,7 @@ describe("workOrdersApi.createFromBoard", () => { type: "PM", siteCode: "BK5", woNumber: "12345", + customer: "Amazon", }); expect(apiPost).toHaveBeenCalledWith(API_PATHS.workOrder.boardCreate, { @@ -122,6 +123,7 @@ describe("workOrdersApi.createFromBoard", () => { lifecycleStatus: 1, woNumber: "12345", isAddOn: false, + customer: "Amazon", }); }); }); diff --git a/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts b/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts index 3c128e93..fe70bd74 100644 --- a/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts +++ b/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts @@ -123,7 +123,7 @@ describe("wizard step validation", () => { expect(isWizardStepValid(2, draft)).toBe(false); }); - it("allows step 2 when site and POC are set", () => { + it("blocks step 2 without customer", () => { const draft = { ...EMPTY_WIZARD_DRAFT, type: "PM" as const, @@ -133,6 +133,21 @@ describe("wizard step validation", () => { pocName: "Jordan", pocPhone: "555-0100", }; + expect(canContinueWizardStep2(draft)).toBe(false); + expect(isWizardStepValid(2, draft)).toBe(false); + }); + + it("allows step 2 when site, account, and POC are set", () => { + const draft = { + ...EMPTY_WIZARD_DRAFT, + type: "PM" as const, + woNumber: "12345", + locationId: "99", + siteCode: "BK5", + customer: "Amazon", + pocName: "Jordan", + pocPhone: "555-0100", + }; expect(canContinueWizardStep2(draft)).toBe(true); expect(isWizardStepValid(2, draft)).toBe(true); }); @@ -148,13 +163,14 @@ describe("wizard step validation", () => { expect(isWizardStepValid(2, draft)).toBe(false); }); - it("allows create when type, woNumber, site, and POC are set", () => { + it("allows create when type, woNumber, site, account, and POC are set", () => { const draft = { ...EMPTY_WIZARD_DRAFT, type: "PM" as const, woNumber: "12345678901234567890", locationId: "99", siteCode: "BK5", + customer: "Amazon", pocName: "Jordan", pocPhone: "555-0100", }; diff --git a/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts b/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts index b9b1511d..ee3d5c27 100644 --- a/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts +++ b/src/test/domain/work-orders/mappers/work-order-board-mapper.test.ts @@ -60,6 +60,28 @@ describe("mapWizardDraftToBoardCreatePayload", () => { }); }); + it("includes customer when the draft has an account name", () => { + const payload = mapWizardDraftToBoardCreatePayload({ + ...EMPTY_WIZARD_DRAFT, + type: "PM", + siteCode: "BK5", + customer: " Amazon ", + }); + + expect(payload.customer).toBe("Amazon"); + }); + + it("omits customer when the draft account is blank", () => { + const payload = mapWizardDraftToBoardCreatePayload({ + ...EMPTY_WIZARD_DRAFT, + type: "PM", + siteCode: "BK5", + customer: " ", + }); + + expect(payload.customer).toBeUndefined(); + }); + it("sets isAddOn from cutoff preview when scheduled (create hint; not frozen)", () => { const payload = mapWizardDraftToBoardCreatePayload({ ...EMPTY_WIZARD_DRAFT, diff --git a/src/test/domain/work-orders/utils/get-inline-create-missing.test.ts b/src/test/domain/work-orders/utils/get-inline-create-missing.test.ts index 3ddd8dca..6d2204df 100644 --- a/src/test/domain/work-orders/utils/get-inline-create-missing.test.ts +++ b/src/test/domain/work-orders/utils/get-inline-create-missing.test.ts @@ -7,9 +7,10 @@ import { } from "@/domain/work-orders/utils/get-inline-create-missing"; describe("getInlineCreateMissing", () => { - it("returns empty when Site, WO#, and POC are filled even without schedule", () => { + it("returns empty when Account, Site, WO#, and POC are filled even without schedule", () => { expect( getInlineCreateMissing({ + customer: "Amazon", site: "BK5", locationId: "12", woNumber: "25001", @@ -22,6 +23,7 @@ describe("getInlineCreateMissing", () => { it("accepts locationId alone as site presence", () => { expect( getInlineCreateMissing({ + customer: "Amazon", site: "", locationId: 42, woNumber: "25001", @@ -34,6 +36,7 @@ describe("getInlineCreateMissing", () => { it("lists missing POC fields without requiring schedule", () => { expect( getInlineCreateMissing({ + customer: "Amazon", site: "BK5", woNumber: "25001", pocName: "", @@ -42,8 +45,14 @@ describe("getInlineCreateMissing", () => { ).toEqual(["pocName", "pocPhone"]); }); - it("lists site and woNumber when empty", () => { - expect(getInlineCreateMissing({})).toEqual(["site", "woNumber", "pocName", "pocPhone"]); + it("lists account, site, and woNumber when empty", () => { + expect(getInlineCreateMissing({})).toEqual([ + "customer", + "site", + "woNumber", + "pocName", + "pocPhone", + ]); }); }); @@ -58,6 +67,10 @@ describe("inlineCreateSaveHint", () => { }); describe("inlineCreateSiteError", () => { + it("prefers account-required message when account is missing", () => { + expect(inlineCreateSiteError(["customer", "site"])).toBe("Account is required"); + }); + it("prefers site-required message when site is missing", () => { expect(inlineCreateSiteError(["site", "pocName"])).toBe("Site is required"); }); From 800671030f9328f77b89f9226415aadda28bf297 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Tue, 25 Aug 2026 18:46:10 -0300 Subject: [PATCH 2/4] fix(work-orders): address review and CI for account on create Search accounts on the server and show the missing-account wizard hint. Split site-dialog state so the maintainability line cap passes. --- .../work-orders.visual.spec.ts/wo-new.png | Bin 191711 -> 191686 bytes e2e/work-orders/work-orders.visual.spec.ts | 25 +++ .../cells/use-site-dialog-account-field.ts | 36 ++++ .../cells/use-site-dialog-form-fields.ts | 122 ++++++++++++ .../list/table/cells/use-site-dialog-state.ts | 181 ++++++------------ .../wizard/wizard-field-select.tsx | 12 +- .../_components/wizard/wizard-footer.tsx | 7 +- .../wizard-step-location-account-select.tsx | 21 +- .../work-orders/types/work-order-board.ts | 2 +- .../work-orders/types/work-order-wizard.ts | 13 ++ ...zard-step-location-account-select.test.tsx | 84 +++++--- .../mappers/wizard-draft-mapper.test.ts | 18 ++ 12 files changed, 354 insertions(+), 167 deletions(-) create mode 100644 src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-account-field.ts create mode 100644 src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-form-fields.ts diff --git a/e2e/__screenshots__/work-orders/work-orders.visual.spec.ts/wo-new.png b/e2e/__screenshots__/work-orders/work-orders.visual.spec.ts/wo-new.png index e6298c3f9fbbb1d737c7fbf91a4dc3b65b3085a8..e7ee92cbb1e9058a3f6efa84bed6c7a13cfccf37 100644 GIT binary patch delta 44060 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 bK~w_(TyY(YdFXls00000NkvXXu0mjf@^yK6 delta 44100 zcmb@tcRU>L+dsOI9zjG9JyC-wDO$87f`}k`^oZWO)kjGXqIauzmKA07wFJ?7udDYG zZL!PlS)cFkX|Lb&obx-cbLI~_FFP|kGxvR6*SlQzpGjiWB(d+eE0siFd4Wu4S!U&> zAkr&Mof+cypD9X5bKW+h6s~2J)VKlyMQ7IYaM4U@6y{-e&fv7d}Yv)O;F%0JT|EUSBSkHwzVo|pXiN6q+0lNQ{> zpifrf6MB`bj9gdIdylh;;{<*IE4TIdXVvc26oSlnKl~Z!#*}*6P4n&yD|8i1AN(-S z%*m~{m{*o|saLHW%a+xS2#oNlb2+@y@I3c#`ET64#0UI?wBr{`C%{E9&!Cm!-gT7M zx@74?`>DGR^P?RHtZG_d z@97O&6Yes9$+dJ!H~6Gn$NqC8-Sdcv<`gx0v55kj)!%9&_8lrI(UkZfelPCJ+zi_) zJpFQmmNKnXVsrII6@H>7*NcKTX2NPU$*Q-2xxD9K_Cxe%aZpO88y{D<&c)jJ+`CC3 z-E@ndw*udO2UV9q$yUjnvgZL9$?=nlqjNt_n%&jG@mb-I(%N6P0>y2GZqfVxP&o~o zruMp;N+X;ak_XRr`Z9DYg}xFUoTl#O3FiBo9}S=GpaBgHhfqDQJhfo!jUFxzrWL^F;e#k`MzKfC_R6EvU+kOp3LD;U}K{hDSp^tQ2AFmR%E}5 z#@$xHbCVKqUn-PM2#mNf*u+_ZC!SIDeepo4Zp!}2{O(ODdSMYS#=9$|f7$=*4XY3Z zy}NePDJ|hIKhYqQPbKtraOqpufS;!!i#r0*{PU5$A48f!pnD*>cW>0H=bsVnkTFG; zk`-}(A@>Vr_~`blQLk7c88zx+y8iD+ZNDMk>`$*+l)LopU zC-~|^Y43SFUQ~|#F-7N%3eQzP)=1xHS8hLf^oETq;|uisI=6@P2LO55=WtGR4bwEsu$8TD%Yjtf*g%Z~KM5ou$NQ zwyxUg_WME48x{ELI$D!cwTJ>7Z-Mk2)_>`M@@HzV zS~I`u5R`4GLzGLfwLdO`&h~cQliporj?JV7@~zK{d&Ug!X01>;_9fEy-#A{RUn_r( zS5rD(3pJR{YNHCjEeJsFjv|a!=yu`d?`fYerPa%b{-il+*gu?}8`zx)MFO<@@%o zJVo!{j}Q~z4iSb`nS;N^ELGZVoG$xhWiFMx&^QkZmXAn~F7{Hi4lSzgj>K~QaBy%0 zbiJ~Ki>qET6iuBCYPM6XtOf){M3|Fszx&{I~3g3G8I{q^q$X#$=jv0rniTXddC z$*dLYeV&y2Ht;ky3j#cs6V?=~VC5gw;6GdlVxK5V5Zj+-gR+Mw3Kk%Exw;~^7#oJm z);ET>3$tG^+hxYT3(m{t@fcbde0v8mV&9M{-OU-kMRB!E0o(j-MyZls9JVRf4(tod zLf#03*Gga)%@-;=sneZKv#&z~hIO<%UV%Wb!7lZW%`?b@;z>?nlSO1TmBv5%PkcJ49A}k_J-p3Hu#z~rK@Rn_O+*ithwNQsE z^|-fcGud=9ZNwp)REy@-D_QSVsR$9$Ac>UrW=pVBpl7vehjgmJm)51hQpk{8cNFX7 zGfdI%+)_SX$m!C4_^lU5l6zm<omO|f4fBz7I%-6>;M19yOe%pg9$XTt9VK_G;b27O3Tmmoxel2N^COUl00 zEIduZW#>@oeE#a=e5KR5V*i}J_-8paO=!cy{U;_JBU?dY#|h~(Xn^`LS?y|j8_j-J zZ7OvUjE7Ic9vkW*AHL=D(&tk{yvr^15`8QRd=}3}mUIj;_}F>3m_7H)kon368#kDG zR_x_(cFympWR263rgFMUP8L|QmrGUd`GDl%A5XK{bwKTf{Tp6;IK%j$0J}!- zUweH6I(FkXm+ws-v;bqAWDmhh3QN}rlkMdF3T51^habSNg#Z3EZ17nR6JjB74%%uJ z-fXx`*BB*mULr&^{^_<&qv1F5RrOEj2<0B)cI`i8B;ioLr=`M&XIc6XuWTCZ?UxIj-+f zs$NO!RQzwchtAnM>seRywjuZHNFSS*695)rkkSVaspucm1l?;+%vP|!M`aTXi&$aN zs*7~yzkCJ|NVq=rGc~1wqG6cG(NC62IqAnr1uFdK45N6h^B0*kDE zY&rl7rpVkiKx5Cou1cwvoBZ<|v(zk#i6)UGD5)i)VjTYBFnFLA^zIovw@R`0XgB?0 z(@!hLsSPHCKj};ms%?{7ltY{9A1^EUmPwv z?-ql&cD3+z!Y#;yt+p_Z(*^S74}gV9B+PjQ6H;@&AHz#WGc_RPl3@3_%*!Y)IXS(w zh`BehYHJv*SzM}a6VPrTY7(nbW_h++bJw;8v8Ip@UCGXoyGUo46Po(^r`j);}k&GghfcDbC4E++MT-5|JN6e{V3+cOZ7_28Y8IIS}9S_|oUg7q9{kjToV zt-^(9#72Yxxt*+_^>8M;(XS{OrQx#G_>owrYoH@WB2o~?AJs<3=Jv1K7qxBwV!x-2C>eY4 zrohFZq_jI3y`;mZHe=A(V3R**)n?9+#lhfcY3phUNKduWctA;%+Uj~4r+?_xUpIRo zg3qg8on^(`<(VDmQz8AO?0VDpp7uQCAzk_eAMO{HZYTS>%WL2Y2=UGL>T4x8t~mfL z1dXBCDHLRLIY*r66@kTQ^rCS`p-8Whw6u6r%?CFADe8_!gZ^hq;=9V?wj%iWdb8{O zP24SjT|=Q|A9n=aGljXk&^=)If%xx-uF_MYyXi z!iFprkaFJ^$<_=^a)fV-qNXasy>oH?W$6mg{`NVa?Nb`C7xm5OrS~2%P~Um(Dm_2O zVc%{g$)}oPoR)my@;i;v2OfK&Zp+3kIwY+mwyPcS9zJc{&0M6*EFg-Q9iw5?R4?HW z;hvUh@M9Elo)96DL@l%Bbd3~>huJ2ejg zR(skcIF+HA-na3B_Cz7(uO=l%mGhthNbJ#eRr5FN?y*t^y0hz5FWJ1vwI;J#TqY_U zuiLX*Y3MAPG#XyXpt$X*n>xh|%1HQhPDJ<%ukxT#e*k)w>#fGP#;gkRAZz2h3`o$u z<6FZ2?7SXfng3@8q#WT($p4qyJ8{B%1NGgo3>cmrcwks}_&TgY&RHp@eM3z&X~DO9 za$rF$^{M3ISemxNZipjTR3}rAzCdJPN$6o-v&+1%Y|2Lk5?iwX5Gcd$^6K-kRulL+ zl*XQL9AzJS|LV%tc(npsvY6UfcQO=Y_$D2&er4lM+r5NERyD1&NzM#arzK&h=L z!r*LrlX4H{W7LN|)Eqq>%VPX|ynn9FD?c_rxxagGQk6dQDPy4trZg7E^fS@k9BVT> zcD#b9xI%K1O9KWKNeT+^_*3HmQyM;n&(J4YYzLRZjrJp zov(%70N78zW_;HfUz_A0d5|ydI5#nUFy+qM?ArOWlaS6+3U+cMjzMz|o z2kpP$!*}4kQHeT<38J6mf`1Y9`|Svba?^eT#CQDPUa%SVYgqG@3Qs(uJ`dgaH-?7E z*W>@-sWC?7^QB(?Ze{4z6%$HXifs$&>~e<3-J?RQ?;SmwU$fOjDyZM2n<0TTKDY`8 z8_M4+s_?>xAJnnj?z}z~C}Mme&M>j};ny_8QZOOZ-@(1vL=gTm^xdJ4BRa-A;0!yzV|Vb#?fdA?}l#p1|J@XqUM*Nkh=mzHo`^p1&ZYR|m$)1tr zu#awtz71#1p?ZL!{`ypQ`ZPdR+EiOP(~gD53jB&UF*5%a2$Xe&kj8CA={A#^3dfgc z&gSajo!4t?Nss5l73?&Z^+iScAM57s!6Xw?M9W`WaG{QI^{^kP#@E6CbWFr1iOm(k z<&ohvYkGXpupPLwg_?VMBjRJXjpN?uMi1HM0=uw^AX9ov>5aU+nPPwc%D{t=6Y4R2 z2_x$_{wtH}P|X^Z_V6AB^8$>qp-ppjd-z8UZb7@*`bmkJM?6BR<|mpjIfv!9aXcDi zURAgl7K<2m@TV(iwR0%|!cklBsC#gEs=>>pjm?N-r4zPOuR%XEDk&oHLTI)w>UMZ> zPu}q!pBi6!F&nv~e@pzMx}r`kc;g5gy~R}Bol5I0=i~6JM(~o+{8FdTYiuuG?D$`^ zsX92BEBP>R60rRgb&MuGfus6DYvds*SRuQ3?AjzniUI61_~jvR-)34@G`q{eTYsJD-yu;6K2ZLlIV*b`ea%6&DD6nA zeo8n*ejJFl-^dZ$(NkAj&bvtz&H#Zr-ptz)t#TeGN6}Fe;__+ndb+jKvgwLxovXDz z@~!SG@7S~s`a)nL=c4RRrQ|oXDYb^$$1$pcV0O)o-wM2*wr^l13%)*TKQGV|{_Ng6 zvSXd-nUR@mT7>XmyG&)m6mFz2sKNjjzH!s`OOgPs--;kBSp5KR- z-zAia4swP?pBqt=#!QJBsY9g{NiA^tchDFEe)nxQps~nq_w^ZK1xprQz83iB>g}Yn zm=9il{u&kl_;^(THsj#838j_f2M-BnQwE43O?;*^U`k&s6u*S%iT!tD7!N4-t$oD+?AU*^fUSs zQ?Mlfjk{sk`*)6*9H24ltLND&7&U(1ugCwqyFH17DSUgPa{Y_F6@*T$WRodRZsLz3 zf8C!PGxkvFZm{~#a$T%<;-(%+#(*H^kCXcq%1Q z*#3*Ft)r`LT0Lj;R6(ngz3R8?KHR0LdI^!hE7$VL`8MA0MnraA<}ZXTzvC*~l*Gtb z+#J&4m}6x(uL0e%a&5)`c4HE;&7vK&`#Jm~@4LWc(rqYeZo%?l+zy08AvEmhBrd^L z>GK6{^`g&@n$;TRG9YoTqnW>j%^TP~ZO8Pk?E3p*VD#rUJnfHX2?dAZpMnDd6pAX| zbPJF5qpZcy>hln_s4rWnN{cp>U~LbvYe%gDv*8!lH^1{zZ;tp~ofRePs*SO^t$@`d z*=6b0)JzOjjd=M)K|woptd4XtjoSQPrtvD*7PtoT4Ji`%DVjno-Mq|SRX}F+BV*z0 z;AXXBc?+twX-Fq$<8>Q{B6flX$lQ>bt{xq*XMH@1l6^}oP^k?bjbit%U)(`=@G1OS z39ows9{Emk6AhGmZG~K$K6044(Q!c>sKj@>p|xUZwh01n7>P=z`JBKa{vvbtOF+?0 z`c9n90Q!(zKxAU=ZPf|2XbPk9glWR2ix(c-M<;=98gtq6X%G<+QQZ>~Fju*vxX= zWlK=(A}&-HWH>#D>HWG~4U~?`hW|ZgHy`7ec63@{SP@WMaAeP+v{>7rtF$aBnV(Le zVg*-5v&&P~%E^B$Mvx_2jwOt-f2>(#M8IGg=Sxd2SuHyZi8>=lSn-iOwwjMVaiDoR zghC-Lklj$N1d=SKpisUCD@LyTogBRi5#0IMk*;PwJnZM^ZbV!J%z-U9KPI&Mnjc(4 zeT^w%>UvxF%@NCV>k1;q`O)#OyAxcg zA4cVWorMLUm8*LPVv+-Q2($9>l*y#zSgwFTp`Xyh^MJtJo?xqSp{F(XlvMpo?T_3aG*KVo(bC^Fylq}hJzcziH1XEOwsc|( z`Pb4>U!xrQ^MAQyaGMs>;!*_hrwt{ zQrx8N9TqE*j^^H#RT;rrvbqtRYp!UE{rc9ytU8%BYURUwo?CW-8GB~qrcRRUEjGe& zL^#2$|k7=0Ee94 zE7J=_hp&MWs3@PE@Gq;wE2#b9<+(DL;>7R^3S(J)%iqlxH^Yo_Gfmr261?IA0ijqT zkiAUC^_w72zEptcMLiSbLiv8|jJ%AVim+FX)A016Ya2%}3kkE+@6X=X!u)CjC^V2l z`GO)^;ouw+#)8CJsdB_4_btzx+qf$dasIc@`c{A~)I{<32Wcz{Rb?nevIz3~*0o7r z+utHAWSnBAB*8TjbFwk`toNPhsHFNiol{(&G27#2LE-T+_gRrQoKI+`k9IMX&}BQl zR0a|F`}asMe>3R~jF4-`zCw|`vBJpBD;CvvQgwwbzYgaq;i|m!{H<+sB;wy`LT7H1 zdL97HC5PYw?HhwBU+6^7h-Ce55qr>Q%Y?bWPD@LtSf8ejcO@(oF3*e9dk5u32`c2>q!7?iD z(z&3ll<_SRNXEr`OoXvwq!$^_a05X=l8f$1@&X^}EV2LHx@)!_(o~`}A3pA*p9kvm z2qLY&N#YzFV}ZJ2)~`9Z>>yZhyR*u;`&fZCB`wbwlFiLSlRVRRBWh-_O?KplH*gjO zk$x#qL-)<``3(be%fBz#o^2J z4XQ%J?=ud_{#UjG{EO~@=09esKW_=ec;sq3EBp5j$|+=KCnKqfQ*u0Hgof{koV~N0 z()*xNsUEyLfV+cUp8GuiW6#*eEF{fxjchR0=q||j1?3gO)LbXNA5HOMN4)fmplq4T zHWBm?jQ=-3YDL;71Ytxg6j}gU=j`v32Hb45zUM=M!+NCKnPj?FEfV}^P18!k1C2D8 z9bJjLABTT2lm#gv&^p#<@=@Q*1}K`TA?&{%T;@Mza53oj^CoYRDR6Z z>Im1S8P|m91>`HL3YFlyyayk8+Y>1JaWJmk4m@ z{X{ge&!|(b2txFfN2V0;pQ|jbQmvKb)$b1nDbZV?>bv8t53Tj#bsBw4k}I208``9c zg|kN+T>|wZGi4O=yqyQf#V6YanstuHnvBzDXCaoo&u2vC6H4ZE*y5k6$%Z2xKUOUk zp1p;rJa%mVIKp&i{sBP;A&3rPVZh2$B9QYte8JBJW_FMVi3uM_JacS&X8r@yi`D37 z8?as-Jq+tff*s#f@+%9pf|2w_Lv@wMNRj$sg22x@i!r#C0phqTnO41oQwQGIm5o3T z3ngrS;I3T>jB;#Sb&Rr3Xu}DF>U(=a#e6VfMvHq*8*9~*0RF@H+g)Qq zYlwWr06~^A!#6YtT3PSg>$DXqaffRQzAq!|fpbg^^)varNmf9sHJl9jmlRT%0pPD5 zZCN6r>s|Lr!s&}oy;`a|@OFic0zaPI=W7>$@(_UrGTGtM*ro@d0r}I{R|c+H&T4Ma zm9wYYRYO^ZHDPB%0_YDweKBi$ZR6C@O4EMA>gRsp;-q|R`7^$xZp3Hh+})`CM4 zpKXDmjUa&hi%TY1I4{(kwGSqL>?6GG>-muL;SN3B4E z#3?}=o!1P0v)${rwmjY>OUGl!d!y1qtH~O7s8O}dhsMWtn=WflE@;Ku?_=ADL9elu z5nWEvnA@OrwrljDI;E1yhPsBLzT>00>TjDcm-?z&UT!($msR9ky-N!QUMhJOQnLie9<^-*-ap*FP0|8(zWLI0b!b zmG7tqZyUBBaXNyQK%hrI9L^F}^F72Fqf)GU*kYGo>geiuPOt43gotEEQc9agsPCjT z(R}PfZou4yel)(}F15;MBdbT!d=ZreftW?q@Cw!+fsfH|o33Px6EMoUAbhpk=D6{Cr3K>+fEnIdPxuaul+zwn{^9e@^pEND+A)J=n4)R-ZQ` z-evuEVG8ac6=3tZv%t_|d@j-^LQG8NE9T6iYU&Ybu6qmkAHuL^YpYUYlhC!rZcUN`Etojm-$#8ktl%Bu`9Hrn8Ns4wM=h_H^#Nw-w zj3*cOmxccQ;L^YWUP17^z6h={4V0L>=u~;o?8uTADs4phH0T!CXK40ORmX^Iuww~Ri z9m-$C``c>7sE3ZctbFX5HentSVhl@Ee_X~RJ|52~7{Md&P;%xbwOL@Stg6kHALktT z+MKwf0-hAhURMPO2-fQnk~c`0n=l21+mZ@KEqGo%bov0TKmt++J8N0>3JEbZfC%-8l(u+I^CQgcu$}KQUW2G zl@Y)6Fq)DAdASImAkd>tasqfKY`z8mlrq9z7UN}^BW%GQg!y*4{ofztKm@8&aIv{q z4^W3#ihr0Y;D&M54)E$)>8DiK^Z*EO1f2NtD#9z~%~vv}N0OCLy1Fj7o}k_OrI1#I z(#CR2B-gZTU&CRbvUc6VBxcIaU~ZY}@)Y|!gZ@`vR~M4NU4F+Gb%Z!BxSWWoZIyMt zErD_o3=1ChJ)p)03EfSVcFATx)poVktMJ;<0B?45QnasH91?0X0BY)dO>hck^Nhn7 zSV}!N$Vy=|YP<2n6+@Jd#nkB}=5(R#`73D&#S`TRK`WN3R$~nk;OvKZ z`-%~lx8fV)Pj#99Iw1#pNZMML*<=Ab@%q=j>F#@A*(946vD@3a^*iRf*UxGNUGSb( zQ0r`2kIl_xpmoODWwL#mU620{yKnGiB`Ly~FR$H?b~>oozI|EO&B-psB~?AFm9)p$ zU+#P|q+{3ESwF9yroegmtqKO4ziu<0V4IR-ae84>HCjEj0baPraZo@0Vr%t%f7|p; z6gqTIH>&drVVHeg3FEoW3%8xt0p?zw?=k`UscgjRSf$U(} z5%q6`Q!lTG7Wr$@<#WhxGC1b)K|KNqnmf4D!pJs7bi~$XH0R-Iq=P@US1=G_21DY@c#l{u1goykMXGN zr0UbQsn#Q9Cd!9v0`K(Q{TJrE1!|hi%=%jTfVIZ}SFYzA``1^Zs~CPOdy~0AM*L3$ zIXSl#L-zHXzG=}m7HC*cevZl7ZNm5yW?s@iB#f{Z>`^&S2sLAK*IjP#9c<*Aa5sD` z##^V$wdUQQ;6Z5`xz*M`0Ve4Q>d2Mb%{ zg~Ay1I#41b$MpPtI%2Zc%xE&r!;ntfdH<7a0|7G{jt}~vN8aI$;0uS}ySw|+y|$z~ zXLp+N80WAXm;e`^u7Q1)kQ<*83Df#j7h$kdt{v;>U+nA&g@iI;dIOBSnx&V}*OkQ1 z-oUdu(yoVc;I>!*Fja2ws&^=UYfz}`dsac3bep8d_T0X<&%kGdLnl-otX3x$B3^I5 zd9s`OinXxTqs=LzcQ%VFtdZ73nuIz%yv$JipV$d*z=?o{ZDIUjVDM&n*Pmyca*d| zlw7pn&lmEMjiv8-Ad+_K2ps^f(XSbcVW5L@P-vnljL5#)O>hJsP$Bs@R40t>$q?A^hdkt+xS6kg$*GliV3$0e7+8I>d9daSA9J%Z9 zH$%^37Hy<%{5S}UlX`1vQ(QQsoW}+1JAOb9{EXy(>)IO1b6m7es^0x@nH3TDo!N?v z8CkQkwNp=f2j+1)gu5y688^5a_(lrO{KuEuRFPM#%4tqi^n_B^bx9AMY3FTj(H!Ny zq(?&y{Mu&8X3Y|`#{U+upE_d7^OVs3^RL-$jGPowh^qnVSUX@mVw4tMFh(`;)RCoERdC{vV@hDx-NVg=K&hRYq4=aBxyNQo^xgzjus>rkcbm@;m z9{h{a)EyV^x(Anv(wyldhMn$YR=wPMCM_Q-D=+>^GCiw*ID>8l^7G4;kB0_Yam)jjhEA3`L8&9JGm#%=7Lq#DrhyFpnYu|H#96FoBf`7pbN zOHXedJhTm)Sr}1?&e_$yC9UHWy*Z|WO1Y!pxtNYi9Fqyne&ZFZ6<5~rmEY;?TR?}B*ydqns1O|YB+3lA~wJ- z+MfUzf*9@F7)-!h{|k{_0a$;4+4=j+Tt9rp3OI%SBwTJw&`+Z0Z#PfsUj zONkBXo;zqniR-uKDb{92j(7k$oUZ|4bYS#~V*=>9SfX%tA zoE-nc7cycezm;sUp}#0~olxIW05{Y_dRnb2>IY`Q45NOd_1q}$qh0W^XIC&5zs9k2 z93?8|Id^(yV|PpBVsY}sVEf+ez4R9*GW+2o`27o^Z_kap#8sV`^I04*zxHM&pPrru zM`9;SQ#4d2B>6dcccKd;bQgh_^I4v|h$qJ6EHF$pd3;cUBBK4o^GjznfXv4$He%2; znnAFaBH$UMn6TpIT)6Cnx&;5FLe~ChmJP{hkv9k8l^+kaeV($)JvTd;9kdIBG?l(c zDdQ%pmhcnR$GRsG32#~4p3U%^En3RPhWlI$2Ej5lN$5I0Wq=f`LqZTLk^I0}x9gzt z@2i+66=Tdli$ebbw8sw*W!@DRyZj{JH&8}3TN-$!y6+MN=if8>|B8M89U!+62r|11 zUX~AubFc|TlYf=(?P8ML*o*=;$|Yvi8{Jzrd+RO3r^-WR^k12(Ref!2(t{H~B#920 z+j8Z(a(26Jv|Yk=d#cmC{RyEoqh3pSE2t(2RCx%!Bu(Rzg$B_>N}$)J%mnM+#!}uU z)p281vf~rfJ|`abbujYQo1+U z-D!;7z_aB@G`~P#TBG532)~QjMb3&0vHLcb?EWR^8U1yvS()W!{ZxVd%4_hs8rf zvnX?>xWcO#;(v(^ir+S;;BdibLh2*dPSc@%)kk7wCJ{YxsFI=*3IXPmsh4c0?{K-A zcQV5Mx6T|Ylv}v__eZlJz?amVDHW0Q0OLbaX=mVcy_9?O!ce|Fhk4v`ACZ zeodjHdh&peLH7g)u|h-tPzY?{Fl%SiGeqkx+B%4zTz5dJLlRsOPCyhV2oZEB5AyhK z%_NFN*RA1>PLaIjlixONBl&7JZVIh#g&pzL?4qp`38Xmd%XK#HUuxBg+q1V&3Z001e{ zMYIzdikWuiDrD;=o!19`XzC>;CZ2ZYLxae;>G{9-UOr>OB@edusI02OZSHfN?4j%` z*sRFLOHsdEcg&O|_Mz%fjowsj!XAL%U39qY<^h^# zA)A|<>G<}3Selso9MF(3{WD`G{0}Ao%XK0`2N6vw@$j4^HRE_efbs9RFI`H-h&rjD z%4sO9Zs_3nk&dWyQ{pot55p$lR`}$has(H=DJlHfr5P@M zV`ceh59l*kMLc)u65!$C>DDZ+oU|4ZnZ)m|+{npsZun+8I$Y2dvh5l9 z(ij**NOG#}$TiA_jha|#dX;Bt-=!6I82Uye>$T2J$1M~Kd&=X){heAVncvntWZ${M z32VmvK@v4y@F4zU(#uyIg%15i-{l20#ydi(8(ulT-*3Gw6>|;rxmFhjoEx?ARQJTP zb|x+!d*B7}VOFrlDO)AVe<=rqSC9=DAbrThkLIITWO3(1GwyuItXcQG0@y>G z@YR`*ATLgv6%+2+@jcuta9`@gd$=f=WK%f^I|iDr-+s5WE0pG8+3Z^T<$OH_Gepe@ zm`+Mq5&y)LC#0RvE`DfVUnJ*@>rV4IB7+t|j^}u}aeq%|`bSbiv&_yF9j>LGZ* z`s(Za`xJ5IJUY0ee33uD-b|Ia0^RX^lx)hNrb6jUfx>m&zFKtuD#z~36~VzZ-NU2AwKB*yZU9I0BspL7Oun20-IG&^K3Hdw1hzFJBu~zncJYH`@adwWqIRFNf~+S* zAVr=_L1~O*E2)Wdb>B_m{BQQGCz@Wwk(Iv;z~iKXNK@TLb!!ZFh&D>$F zs`|5~E595_iF)-u>8jr>55JRC*0LLVWokp$pLrxBAJ zq)U%rDqg{p8zk)gPjk%Z<|e5{H3f&_U3uQhAPNrs z-0Sp;`P?sF98RNf==wbb(rR3jS{I09auC@MfVt22VI+IcIxouam)1cT5>PIC>H&*O zZLM14nvH*3kx%7R-XY{4%wb}(9`Jyk8N{gxU-aGP>vKP9J_})<**zTu_b^7YF2;nI z^%teQ*c)Ve8u`6wiZ9K3@1wXN`Q(zUaGLi*^EB@HBCb7u&wc55wvB9#nF>JERn7bD ziOhM}XvutN*89#%K!Ni|G5@M)Kb7lCC@Pr{L-z_q-ZuutTj+O3eAgf6H$9l$R~PTW z$a->o>VN8Yr6)`$y@{dpqT%C}l;i(gt->|S&=vh@-srP4z*Bs&qWb)>7;vA*IV|zs zu17Vl$^iLL{VKD(Z)jc%=Uu34clh?-4O~9iE>t;Y=%&cq#UHB}%lsl?dd?%YEB1Gs zF@Elg|0fzW$i8T0WmR9?UB$CsG5O^)KECRm3v?TY$aChGS??Y1(oVgBKjw5Ahy-qR zf9QO4hfoy;gQ@7(iOrkI336zLs@YpY&hho5qHmfk9@nvNB{aDaHs2@zp#l@Y@jpi9 zVLZ3$UP?6dn`G{P^s%o9QtUCIM^lhv9~<|^h;8E6iZsN=O}~O`kZ$gCW0!+v+c<|f z4g+fKfEB8@8qvcyBWk}c=DIhUiK#=zY$h69N_BEpB51`|qrK{Dzo#XqLdJ_xy2`^} z`xylfh8C44LoV;=p1`GQ95kaNIajmQJ)|x=JL{R6BH)AG88^siBeCq21IrR){=V6Jk%BwV0 z@FRw|6W&?0)lH)QA$dZp&NLy}btASg!I*REqd`3Hg_~2!dj_A4SUKukczx=JDS{+y zJ$vN(y_-j))LGAdnygoY5lwSqI+>1L8gf~x;GKaVb+`UFQKGjU&zqL<-rB7^5}+)K z4=%vOTLb^@3MV^fK)-&?S!&lYG724VP(S6Q7 z3wMXgr!V6cOYx%M>|{}okBt*`UQ#9bsXr`R=0u0#O)8+v=#X!P^&o9%9I!EJ%z%5woOO1jk5B+dOi9(k`h*?Obn5Z zzw^u_W^`W&tA{enC_Id_NvPn6Ee0%f!pywpI+9Kz;zt8-fVGnwY&63-kUN5Pf=Jvl zMaa`d>slaT&avf*PRkwjj&kc%S)iZ@gP&p$@w#0eO}~b*98S2}Zs3XnCXD@JwmO;i;$#kM8f;w5Qn&PyEU)$vYG`;%DyS79H4MLARmx_tjGlNYfR$T}+ zqE4ZcF7YW-i3=3{J~VoV#Z258YcG$CR;!VH@d zm_|Um*oMma63})ADGV|7L1ywJnH$8m3{!<%)X<5VLT%2;&7Iy;dux$Vs z*6}krR_?zwXa8GomI}3^Dpo(|wi;jqi_+TI3_PaXB@Uea<~hD;15dmCRxynK+52$$ z2bKZxn%C%*o=mPy&*6)@#OG{>Rely}yU(-M&zM&BXTu?ap1cjfMjD-y$#^m~^7MoA z4<3BTVM5>8R!9Rj4+D!_#4Yw-H(*urdZJL)y=2qR>t;29eAwdT=E{?Lm}fTS&1T^9 z>%+&Ivp@Ci5qfB>rqH;@fz90(hl7qYp;>`F_CYfOGgbwk25)@|$U2gbTzVLB6?C^f zqS}(&K6@=O-Zlk*yuEWbL+Bq@Mrky#g!$yp%Zl<+9#&VDEY>SH%Zem9&Bk7-BY0LM5UEpifGCGAcGST7k?!nrdv ziPt@z;f^@>%+e!gj7SkUs|V|vC+MwJi=Aw z;ng13L9c@+9&+l3%YXPFCi^2TEeMqLO-)~!)Az~Rao{NZQ4l6_CK}MS?K~NH-fMMM z7RlNPq;!I1kpi|v$Ugf&uv;^jYrAecYbGHvy-Tp;U^UP6U`lch*M^4)@^F_^qES7a zm2mm)DzK32fWhY_jqfRhtc3Zhq~NX|U@b4NL=5}vvcp$uRc5l{ND|k$^siNIS3ts| z|5Fzb@b8Nrt`Vx=Eny->py^7=J^{ka7ZVx-Qp!BU=QHH_FEjoBsp0wWqR;MMKKJ;xic`Dd(^^(}@?E{H#^~V~sD@p9px!;tN_>I5Bc*p$ z$gu%XP)j{o)Mce!FRn*bWHp|=k_*vYa@*(6xN+>PQqa1+~s5x=Qi8cXW+zTEpxux}^opmljuGz<8; zyweAeA^D{9K_dmuPB}vFqN}i1jsBhct9-yDjE`VXR!Fbm0euSse!<2zaN=>$D4p>V zt;4Yj;*s;D6CWWI+!P@SZ(S{P`Z03;^5kGBq&wt$%B$@^^|pxDz&OmR4%R`j%Nv;t6G5v+* z)+(2GV9mMwp{fa@Fi7uNQO<|m+<;NApk_{J$>y)E3QEK9sZ7QaRp}y;m`_8MW-|g& zRNp0({?&tX=_F;C5kkv3LA!dVMADAd=L8@t?TdzIyba|t&df{3Ud|3* z(tk3kh)qwJD%UFV95RZ@7RDyh43|I9nL6U0)BW2+5h^A-`*r^KWDXWQH2`RukkGO5 zdCR6f6YWjboZzinz=UYx&X>6^{JhVLMOk?71i$L-6E`vOOd+h#AG}WL)Ecph@fPiJ z-L|VF;pF1#_;{dDIA_3TM&{TmB+AXLuA?aPq2XTGs3+G}@3&7HLrym=DCo#`=k_#? zO_gZ^&bzIO=k$1<2b!{O^p63-KObYB)1_jI)Pp3RJsBdJ9{h|zShw3&rzrDhbtEe2 z$P*moK`I!}6dSz==?(r3PukRaZavKY%KLtMR+=E$zloLfYI+j!%nW6JR8HbVzk3I} z4qzF__P9&Y*V5u_Fi~TvEQEFm;(uF(b{wd1FO}r^Cj9*Ky9?k^_J2;h`Z)xsz&vVe zV?e+R|8&HlcTMjzYKF;{VMQxb2C{q=>0fO#z8rY0ScD1d$8DcjiH9(YS^lYJI&15d zg1?$Zk28871XxN(i8DSk9$P{tK7TS-yII&CzbbZm$}<^v$mCh)*hN96WFL$n>AwKZ zlfoQj0&!$-X`WyBiOS{^=rzU+$rm$hlSZ2$w%C`B+sb*fA7;WQCZ?HsaT@U_`2S+< zt>dC<+je2FP!W(80l5_skrI$Dl@bt;Zny#I7#hh1(w&k6NIBBdF$_pcGt|J)-Q6+p zt?_=I_qq2Q``z!i_xgw5kGa;GS!-Q!p2v9{#}(cua#riHT)fRJGl1f3=bqoiYlq#a zde(G}-p1MCFvRpx3S2^!Y~Ifm8bF3J+S5J@`|8GyEM+U0Ephn z9f+z?i|AL^ZCrFz>BUVqosZU4XshE^XvAHObRo_GfIgB2$bznZi@TAC!b5m(r_g#Zq7);wM!IC%C>M-phBCHl4ozBwzz?+xj*_aHod;<30Au^D*7?F-Z})*p`KtyU7(6^_4Pk+P~YbG9ia z<8N&>TtsgcEoEjq>rYfvhzpoRtTV5RenN-Y`4R#E$9C%GxrK0g!@;3_`wSJAWS4e) zL&?&5@qEC=44XcFL{^c)l=h*WYT3JTedkR-=D`#Z?H?Oi5JP`AAx-TWRo~ck7d8*b zxR24>N+QIiHvKIkK8TrUi6;gHE_&aLn#QE5PbVIi!LMeX*Ux8cEBvx* z0G=(QOEw4+CN>;%8*$z^ZEKtcxEHd-1awy~7a9zfV%*cPHSVv$=ktoo4Y z;SaXM%?U@MJB@ph&b;>?=8<#U(&2EhpDjt{ANhG$TW1x04Tx(@nDmtEY&?$ltaSO+Pkd*YA_5No=)Ez) zvyIrqyA?^@&2%t0eJ~}I~!}P)p+oH-Z+g%!}HnJvtZ$-YN0J-JVzZn>8Q;Lq0k)cJF6IC0l z(`;IG#x0&%%xNq8AR~E~neOuCjSU;mA7<25R@Q2kC)jkK1xae4~S`oZWXVi=swnz-N%X|Y7BuPokWPiB})!1ZG zwHEl{IT(MYp((s~SdvEg%e!dx```znh@79q>$xoX&YPMS7YRZdHCf z;FY@fOXywm0qrugq@mX?tFSH`KE5!LE0bw|5+nma7wP(8aptCXL z)DilQj5TjXsN^~hlmg2}`nX|$?E%}Rr7m#!Rh?r|TB`Pb`IHE6!FUzdU z;dk}^7m&EqoXdZikE|5|w(!c*2DeS7FVZ9BsL9NIi2I<73p}c$cTGfI z3eZo95(X9^BIoXvWYwm!M*QYZKv>-^!tV!Dp(2MZ%A7KDW(QrSFry$O&S<(eDLvQM z6{1^bi-g>Qx*UexxhYjycX?TKz4t7j`WWyk6R|{h+lk?SQJ`dVd#nBWEJ*XHIzA+d z{1;P_n6`-SNyNI)n8QjUavD@BHM(|9yDkTR+4W^ZtSyD!vUSFYzk-XnFX5%HQnDu5 z_YFNhD~Zz-^7L5UKBQM0l>#6pK6Wk6yBZ<3Ej;Qr#a7*kd|r;xVjuZHeZr{TAfWT= z>0Um(ltRTzV`R59O0#rjD- zKOSCq!`cY37=A?I+5A3rg^TcXo;}_$&X=~AW3V*I(S+4 zv~d51?OIC?MMMh7fivaC7Gj+L&5Y_drxaV{w#iDngXKTYyl*x(1lLNi%=vd|{{MzQ z`~qYLLs|ajZgF^bW+CWr))sRuwk>mHE3IJQ*@UI_*eZea^~_;nYYcheb2@6-SZOE= z76yq1#IsWA<&K?%_liH%i2I|7OUFa3wyZnqFHz0e%c#4B5F}d5Jh> zzrONBV%23K2sVz3C&eNkn?_rL(~mXOK)i9X{yPJC?0^h-TR3@!>{QjB>^Pm5EuzWg zg?~^*^?oTvlohIFha-;08C*mDBNW`Rl^0?Us(Q^BtX)o%$>|^ks(2A~hE1dR8&7uP z)}{_u;mjkbyvoM;;#5u^Z5uev-8H_#FNe~U_t-m}QYJ=%10GMoF1ojp^S#Y5* zD@=OU9e6`Zc)RO(^4C(;DYg%8W1-syn-3wTy=jTM6$^b1sA?7Az0pc$yN&Q^gvd0) z;D=gkKTWw3gUqa}oxNnN;^Qsjjoewmn zKDtu2=&5a}2kHfEx>=`FleOU+tQZOF1bXkm+=}6@=ePJ+%dc;LI%fB7A`TtQF_mJ$ zDGw}v^fOq=vR*^mm|A#1S)Gw_$zYYUcbfNdz{D9^=3#hUmZBPm~OC%IMAu{ z+_7#r=_OAJ`w5Qg-}G)iMHwwGwLH_&Z0)vDf7@-HGFf@~id2ZZ>3C5x$C2Y0=;|_i zwvaoh1TT39fSxVdR$BUtyMe zJ~Yt)raUX*95L)rFR98W&SyYcv~r+sbmh`8?2)2J4^N!9{2G0yD`Kj)RscTvC~?ky zIOkg$vvdqjuMkZM7zoAx1t`MEE1RB~iMsJKe;AGVrziyR$)tM$KF?g3BvHBYyoL(? z#1`~gnXgDsV3L2LuzC5=pi92&COuwcep^=dxVwiO=`f(XPLjJ?xpTi+Svg0pPkz~u zX~jn@5RAJ7S~|+Nz020~6GgAWRdaG2E)Uv&#<_2wH)aQuLh9@SaB7RS1NDjY!2UTk z>`Pw>|5sryx(62){T~vxpFZ{8-t;r&w`g8F@`+s|gbki90|`quleL9CjEU0c{Zt|z zC&Dh%=EACkj%!bE(tr~j=>OqerdfGxO>>D`sK6xZ!fFS#+#sJzoNU+G`;? z9WTvoE?B=WkppwFA9#71lc4s+i__?gAT#CRu{F#7LKvH4i{{cG4$z^8sA`%#JTpoZ>h+#nvm+alix;lf&{8-wYG?Ta#Sny0|xnb2FRs(81E z9(m0AQ>M90xrT>_`}^PYffXg$ANt)%zzm9Fl=W=%R8>`_WbNWbF3VhpLxdTmUyZo? z8MTB@r>Jti+W;%|!)NOOgm&TuXWZ%l&v6Z_G97T*aWchczNU?i$c+@@6Jgl97v9_H z#xEReVHCHHpg(frm|rGR=;0s?T3Rz!5U$7p?>Elz+^k@_{^ZoZa$D4^D@u>Qa>BS+ zR9j$fHeJ5M%G8dg1IKnL6f`lMIY8$5S55Pb3rM(sm)n@@GB*AdPIPsaJAHBNaCN?p z2S~!F&bneI!=)vTFOQ*tjC{R8AsX{({_Nel+Jh|pQqUPNPd^KAC1chWiw-4jJjE7$= zM~ZZ`mnzA1xy^$fe$vHk^--{&Tj^(6ulNNkOA$#27f;t4BG1Py^YRnw6w|q~Px5ws zV;<&rjTD`H6svgf%55(D&P`LvyACFHH9A)(?IGGWNmMHq8-Jt1fP@|LDtA=~ZAmFB z{FB-K07``AVm0o~EPy%lv=q@SaSjyB{rYKcF)OgrngzKraXA(;(3daaw%cN8K;zak zOf7T#G(7D?qM;C4qgK$|QQGJ7o#=^re0tac4QtSXOsX z$`V%!6QQ2&Y5+Hy7`l^??=F&bJ`}MIcqo(rm%f&IfK7(!045>Ok6go>Vl|?qSG#)| zX|&8~!1-Ytm1s>>OEUGVZSAvj7zHndu4swIu9g1D+p$OL1LBW`hAk7=92UZu4;CrY zJkt?n10vbgf3zwe4M3Jvm)=8Fpvnm2vB5|$udZfMmX6zO3Zf5z^I#p1$!x>cn%%?Y zN_96H`WsUA$EpAJ*l}QQ-o9<{^TGfH*Qv_CK%p!G)T}y(8?NiQ>BUhJi2SxfbSU5` z^(JOV|Bu4c8MFlT!uq4jOL0#PpI!QZg42RhB>e$Cd;S+{eSMw09BNT0ne#(ka(>dS z#FrTw#f9YjP=!llIb$P5nXhZ#SogsJrj9w8N(rnWC{@i-l6#1f~-a~j*god1ll5|Dn0qOUTUcByWP(JEy`7R7-PUi_afqPWm{j~WRJh}(qr^Su3a7# zgBZp)TEITU-fDQW=#Aiq)ZWsY3K*;4Z8K%+RJO+`%iYwPYDt$I*@d`;6QYmjQJId$ zS&(tQ7|>n& zE-l~-fp}i@e*SeX zFZj^duHXKTeEE5QnhxP16qp?#eX#XAT7eCUpG+>GL)Yu$r;B8f@7!!U=1L#)Qs%Vq zZn-WeEmu)qEqHmh+~bFxq#2$bD+nJO%B=jn0azg5n$LL6Yv^W4?TOH2)r6(2~C;vsnW@IXTEgGUZr` zit=e~b1Lf}CBXDlO@;O_94aK^8N(#Nj_ZGu{QF&{izqJ zz{z|rzcl)B`A%$GM%i6fR77|c0aa|tOe#Zq0Wyz2UcE7B>GB56x~`b?HsyU9Q1XzpeRwFDlq&g7-0fJ(`a`^_AHg z(YC`SDm~3zco8;@e~&&{>62>o#dP1rpP@Z$jK*Qi59d_IS@x%Lflo(C=>>{$pB|f% z=|Qv7Zz*&p*_Oa^_zP((&8UtHD$m2xMmz$7nVH*)xeU1*nG zHik@bo5a8BGCRm|o+bX(N}iyp$7Rb~eW}PeWYvWBK8*kBaGqQyX3e~}`l3iXR4gLm zB1N9QS@TupI|VwW0J89R(^(3ANpAb9@Z}4XXAX~)q`cxAAEUrD!xg+MKgwmH zvl$NwVjGu>6+siC5*}s%i$c5es@3nn(lM4XPk7q}LnJ9x1aD#k4eq~{Z2G+p408_CN`8KJZ?s)r8pFW-NI=9ZGe9F&RJIEj+ z!r)*ATh5B5@eL?X<#GJJ0zDeYV5}1H)pN;<7(J0E8aaszdlzWx5kha+85K8n^qRm` zPw*i?|9;`!EpIb%EE&}NrQ=Fe!sW$hXPLT+3hudk92^{C0Sgr_l{9>}+(DpK!< zUzUUjNCFsSoPYn$VFasn9I z>$*9&TmxRaW9gDB#=G`9W#}i~%0<%FNJ3Gh+&>$YQ2y_Hu!V0cS7IHYBcuMcF1Lc^Jc@SmvrMm)V1G?fF*QGE4eQmj}>s_x~^n7x8pyJ<}TI`E*gC z+-A~M#Llb2WNv7>nyhP)eAzUFcA^#NG(w_sSWl`E$y?W+8q%{!=BZ>)~RpwhepqaZz%^`!r`%w{}EP@;Pj} zy6jtTy?Oj--Ao}v%AVIt4`B5(<%Pl*>$rs_q<*94A=!B0LLTmkZke zn9EW9lXmh1g4^Lw8^VH}77(gG_iiLp!Ywgf>okSDL)_HZv zROPg=P770wvQ2}7Ol9Q{{IVg_sfix|=q9|r)hin(?BrBAB#u&7P2&iO8vI`wZ(8<> zC7$xDYU-C7PW#Ep<-5G!IB?z#DgS4f1@hPwkXJfGo;_6b1N-a!;osZ{>$HA%zflX^2&vZ&yiRMFAbJb#ICrIY1_|`uZE0n zE9#yn)YWO->y`Z?tk3)1?#!yRAdx+;hY}qa-ltb%sO!JEBH4G7cVYZSW*Syk1AxLB zEb!Ua-$?TMZ<3kwCwDVO3eMNlmmv;axte44)~=}#a1g5dvWga#*>Px1EK*kuuN3bL zS!r*}^j6SeEIf5b%EuuH z49!bFa_!PJ8niBA9vG=R8=?;>9t(&W)q~yOcXyO_UL|pD`sG*)!(zEn|0c>Q=|2dJ zxuj1beL5}H1$xf5zm0k7@NAkEGg5sAxDaUmJ5J&jy;hW$r#Qc1KIY8eSlh)qzNu@b zHmi5MENVr&-z*q-XOo-#Ld9RsY}zfVgwu zf1$biyW|eQag(VuUPM2UuNs%A@@sf2-PgIb!M&B5lM;t?`561#-}^oYLC7=myT1>H zj${SBJq+4sl*@AeO_aJ0;@E>y(sZuowJXH@n5JD}5752}$6F)_G+M$Q|BZ1407mHl zt`!Kh_gjOW0yk|tQw4v506GY(VU=w>Yf0Fqy|1uP4E6x+YyWGEoPOqaoBys6fYYy> zmKu7wGf{dsa>XgnvWG71V&5$Rd$M;Cs0rv7+bX1kOdJ}o=B3&o<WdB>Tc#8Xe zvYbqHish_L`zr+w#ZMqC2%D&qS`NiOC z{bp#%P1P7ANEM=0)EJ4VMO_n>>g`*&GD+sAYPcK2y0f!MARqPhJZW#4IY*N2==2VmwKpRs#ufEKg+Z62uIcDK6OCwaXt9juJ;I)+ZnVS}Ig{N_VH%XTlPaoD8>Ygrw_z;5exTdA}+s z7LSG|dXORNX0E7;Zqfc0NkXj$ILU>BH46~PD#Q*#yrJ6S5#En-BGZ74y41H~o&6lk zjW5od*Cjeq?axz{ODl%vnph5#czHMIxNuVk5dJ?I| zN?(@?{57=$|C-yM@13m|gU;Weo~u$}adD9;gwt|_#g3C3rLI20%514Ka z&WGq(Undd}-X(<7uMR;3L3*MgI86R?2PVbxxTg4m2X}l6vY8DVAQ#9WQn*Y!xpt-% zFfq9GV(FEps{fju;8@0X?7Cq;@FXh`!}N| zVWpi7xA`QZ%Hc3N`oCu7Fed2)Veat}cuc#ctE8)6aH*_zfMnMT7TMwL{K*w#!>&$+ zs$f}|LQkwe0J9jRbjsyPhKO_h(|7z9tNVJjPn#1;_joRp-T9^OQDjD~V@I2`D+86u z!~VMMvZ|3pO_~9nhV!!4E|GDR8MT1H z+z?XCLszq0Q&h8V&%x7h*DtsAruBK%E43n4W%hdGfLdE)^TihJz-OntroV-I#T2Wp zq@fZobFCoYt`T1V=J?I7#M7OgR+HG+>@be1iw{o1m9~o!F@pGOT_U+Ek_*+M`gInv zo=T0`>%~;DZ`Q!c_bs*;urau_DF9ZUP@t>%ip>v{eI$x3MSv?*f_CLtj}6n40dWSG z0ZiokyZS}eC*ef*8j#iL&VB4{D-EWiLK6)GDZs8bP}8!(bakdYjo640^b(jtG;|hz zG#(G;?T7t}K%z_&SoOfN4@61QV}474V!Jnyueshi1M+^ljTR!bA4qVN|4r_f*@Xvk zP+_Zj9acp#w5@s87UMP+ugGWkK7<4!{@B8EtZ5pdC44q@E*4O4HgViI*vAqZ3R8HP z5zh*^Uk0;s(Ov45G-FOD_+^u&^m)zwOy}p8)19vtqSu?zW}v+aT#T){;`?{k^U|C7 zFQ@}4o~?$<#$R6MwuVcf6j;=R#wqJRRJMP~US{5ui_9KFxCb&Cy?tXAm>ky>>afH< zrZkzT+~C|Xv6xr>}3b+_df+6=>69}ala;*^9LX-N!`tz#?F3Z zzfL!z=G;BYxT>&SjX+kJSm~?fQFubajQX?;bjsf?ystKDx}mQg@g#7+qoE+zilzVZ z@pt=KglJ74ubIEYm!p2o${ix+L&y1A9!+n=$r4_B!abx3ZEBXtZnr6Xvb9n&L0J{J zD-}ttpZKcQOuVA~(UfGSS9`Ls{p@jRtWwL#HI6Dr1y2J-8B9bDa~?hfbLu%yi)NX} z-vlXufilzG(Xe%dg7ZeM%|T?<6UH%zyR?rIUMSJbzR7_ahC-ZC`DfbkT+e#zT|dVU z#*v}D_FFnPdzUi8-h#0M3m9x3kO04590orJXv^x$`j?{fY{0SCAocSH&DDRPug%@3cJz)a$-MvU|6-h>RYw}CwQUno z1^bn$7q868X(jpfF*8%?+DQ8bR!4~Q23!iOo z%UQI$nMmkVwI?zkU>?d9^)wjx!+I_v;gRQKTXSPe(%Z(rRAV`hSiB70EGa+;5M7V zt^TS46x^+XIl{}#zCZb`e>8CXj78V#hs146YlHe&fSN;}m>U>9X_?T;uo&Xd=->1jwmS5zooUM|w)vqn(x{gRbaD(5<7(ICWn)>() z+nB~+?{LCm7_#beh2ngfUAPJ?l^os~oqUz2!OM0*Y~C zX>J+;l6)U4rqEwA$104!a2d7HH+Ze_Uag*Ap@{l^Y%@>Jss)5~*cNK+)hKT}3~WqP zo8?mYnsEfM?j1NkU3{O+`Q5Ft3{oI$W@TaIGy)a1-~DqeC}yoYm&$TyZa(in_@v>e?6|9GhygsSN1o9wiNO6g}r-lkxyv zs+gEXm>eQgUb>Ie^B0R=Ktc~%o*KVH1|0~ueUpMtO$j=^uf8%cUcI;}3O@&kO63$M z^Q>D`%K!n-IhQ_F+E5af%l9JB~- z5VqaF*-!nis_K7jxd+40s(!p+u)BCKjXBu3gOrK#5S!6^!1aGhb%qW5u>K0ByORZ7&&S6xPFi^n@a&^ImuOR{&zy>2^^EqZM>UCiG zis`Bv?C2^}j{C~V=#7btC@v18_ivxT*N`*hbY+ZuMa_iINd?i32aO!i~?P z0-0E*Soub9K7lSG#j(AHg}ZRP^~$n0EPWms04Q9(H(`y~gz6yKdU*Qw!EHF<&pTMs zD5x+AU#Big8W7tMz+uEXtJ0yF@>{}IQGbt)Y~Vi;K{PZVXr(pXrW=rd@U=x$+vH#D>#vp(r3%s!1?uNA)mzVSa^A zJj1Uni=952itu>;*(@c+!+Pk6cm1_KsG*%%Kv(bXgVhP(un9jflATRt8R`Hg>{G6$ zC1muIS6Ou63T4|{L)6#ZAnAcAOUM%`vaws>GtuQZ8CmtmBZOSeU&h<8O1HG?bJXk@ zl(?X$EeOkRn@XG(_Mw8V1f5pf{Pz1W`y0eu`X`1C7QCGhIO~*m*>T>yktagJ}^Kv?Xw6Bv0m1V0xd15q&%#sp4T^KCXt}_0Zt?zYe zoRpMw4aa-ser$s1PBQK^D55!DujiZpS(&$a1)wHg33K%kr~hckkxrStIMB?ZZ6+CJ zucYbIbB`l<-c zvEok}7O`tgE^Ye4y$1QtY6QRPEFG`>z!ww0jR{n~M-$I)`4iayEWJRD(4e;0?2^O) zLOLjXQzLeYq-8hNZdrF%A=3hdDuy`PoPDcchAa(E2C8{Ft%SR?4RA)3UjGOJ5m-7M zV>E@Zii*nnnk5VR$(IxM`|sx?x>S!(<`oDZyqp1FDP-b@-DK@y!`0*<=KNT{a^pJd z`MY;Ps3pxZ=gnkpORxR(hAq%O1oZ#VKW!I{ho!j}QA8JLmK_Q5_9lv80^;N2rGV3X zdZ~yoDq*)x)AcF++Ku3L;7e>QD>(G8*}1wdKc%}o^S#3788Nf>!qBmMSPN-?8r$J5 zRxtlwFzv;%D~Qs)NZogtI;isP#l2q7#oxtIH0C-w81<=_Jb`klm3y)rSV-L6`;)T& z64SuNHhBu*QPL?E)mZHu+AsOgw-eHY({bDPSL3qs_6R3NrSLOf?Vx*#_8yIT28jB9^~?+?N$*$vn4cdb{OKWKhC)f()#KZ+HhxH zf$5JeiFu8$bBa}&ga=SrQnHKAReq+BmTgR9)E;(v2wa+U#j~>`9P4%%=&$yt8$hxN zW+yB1@?yCL?y`Lf1MERcSsW8;g6CY#Ism12wce|%Z!Rsh%9u{WXFJ1tb-DmrjgbRW$9Pv;D+FowxP@!#I1Dw5pFzL+Tm$d=KCyogQvkVGGen~J52q4U)B{Dqtl-gTY!gF(wFTApURhV8Qdtp_#H&$aI0(~ z)*;-BtQ`Ic;T{g9^O?BZ<4+(l_C1Ts4BKcCxpd8^d;|KmE-SjtFVsEJIUH}o!scs$ zN0&geV)PRo7T znR%mCn=vV^AUkN&V1Ci2yo%h?@rWK{T^!Mz=H60K+d|m@M|2IhS$Nux--2GPuVwSk zH7uJ%4`oMW7OgoW{4S-%NVGed>+RMewNi%zJAud?!c*AEiG}r2d|va}yUcn)R=F!O zQoB18g}u#EsjN$>w~*71Wur|uP{sYxQBF3q?N^!)5-Uw#dwgDL`wEkaaB?`fkF!Dm zPQCJ(EiEk*jlQDVRp9=SnB~{(Urr$`qX_roW>HMXxoiTf#?De2p=#?C z0`Q@fQTjTx2nS1RhM7COcaSRT>U08*)jxi`PI*L1y1!RfQ_2Dw#Ad#ibv@;klgOR! zSU(=)DT|c;JYT0T-}ipBdcS0C3GcK7_bR6~Rs)_{Zd*ID|N`n_yWDfa*}{dmt6O_8^eDl9_D?>(HaM=$2|bmuXL!b3^vW zra(H+i;I0=XiNWi69OC#ZxP0mQd6Jz`x0H6_u^k2f(G=$G_TD18-#2doEZfL*VEj$ z&On!vhVw0VbTf0&^O~hKa4Rcsv)!Hy{hmoB5vO0iD2hqPLP#g^u_X{%**DCdx7RaG zx&5Nfe1(fBUMNdJ^kN{hm_ksS^sK7vd%Te)uK>||x`X5PvP#c`Ru>+ZI>T)r-E+`l zY^RYqRHR->MO8rbT-50+gel##eBb5zUAp>BrD^iXVRn(Rv*R^3C;l3%l^TX+5KiW^ z&fW`=!4>D<9P%8!VE{N<-F9?{3iBdt{B73DTJ%5D+UY>=Io*J%e(XKgC0f>H_Wzew(C97=~m!~ z1@C>3ah;J_q7NH*AlYYwOyOnxR^_37xXWZf8+TRe6f?=K=dPKq=d(Mv=`+%m5+@b- zsCj5boUU7_U{K=7@&A(;m`_5?K|ycBb_v#nx7_;hkpd^_y;fx_C1;{;HEAOgdizQXQwucO3_Z{hh5*VCaea*YzN0 z!{rwz8q#08;!Z~Or)_<3yh%wJvGLzQnoyw9WlBDUm)?0}<<+@i!})rEiO;)U*@%Yc zs}@G_X@uP_PVB(nVoPCWZ7u9pAPu$v zvja~xP@s;f@Ou^~UtflpGLn-cAaGOxPxjRD1c@T5 z6-^3w`}du3Jnwp5+L~NsVq(Iarw7gG$ykxjQQpJDL!~gX`7F^4a`(4gJd%om*NFPn!5(0G9n$g`XgLgy z;oBHQqt_0NAERk8H|uFy#!YnCiW7SC!3cZEe4F!mTj7*av&;0mSDy)ZFM4=cTq-#4 z)!gcD2QT6&H?BrKqW3~TmY%RU+muf0yMf4oTlf>PKktCC8qSR4>dcekGumR*yH|dq z^HCnBk}XTrB&g7vz1MEgyiOkmDicBuLnqdmNzJMXGr8452wqLRT-rVBy3qofsLpg7 zP?%gW#Q)@@cKiPLj^)&2&9sX8c%ADj%~)RU=Ux9t@0l+e1MT9);915rdZGqB++iMm znb=-{sEcQ-x?FfNdR^jf8cXxS-eHSpr4QVWH2P^huFz}MAVR2y7rAd z;#mUy2<(s41z1v5o=n`478qYUmVN2DO@@ixV81{9hU&?^F+4EwW1S3t)Raj; zJV4$`UE*-pTO`c;`bwFA8Jw7Z(ro{%$d8f?l$`&}1wQIMZLMa1B1xU;>*$+_-yev5 zshaH=MdIy?3Y#tQ3c+vfrbnnGJrP%rBFIg zP%fDu9{If|uqhRI8{3vCc79Vxn9GDY!v@nHIL>WLosy+AOjbNzDh?3*^(FvvbQfP3 zlQkO+vw+QrL8>Nrv4b==Mq)kLBBZoB8Q6~H4fw`b1s>?o8xA1V#`$~)z|pi~ZQVsp zj86mlD}mx0*k1PKKd}N+(D!VHg@h+ray;Mxt&2p42lmy#kM2GthAF$@_P)3QdMYu@ z`70JoV~9xsFfR4x`hb$Uf8CY`42^pwX?3nDv~^XmCsl4ql8aM=o5q0e(7gg>o5j43 z?IHZnb8xUF(R(_#K{b##F$n>VHTcwmyx)SKf{7LhJ@^MaGHL#MpaA$E`}zN#ps@oS z{7UTce}`UR@sIyUm-_#8C2$v6?D0R{)ZqWwLjeBIHc|MWeIE$)(C&dt0*K+T@1H>! zkb>ZBb-OOp`p298-}j4X*Kgl*^Sny81xCT%?RQ&}T1g`lsh#@-d(zJtf|^4aw*ll} zEhR4A{RM6#0!RxC`w0$$LCKw zey#|c0Gb;#-bL0Cv)rYGxxUz81)kkVovSp`#lX>yb6{=i<%F2$JYAi7#RVL56DmK0ZC-m+rfaQLylwJ5w z?g(rop_awg(6Sc^^JWeEVz5czi9qt3(#wdm?R_c0=l zkavbmR2+c|SL!k?;Jfn{c~-~ya7uT!`}s9lUAJcoh<7or(k*N%o$lrQk1G!QVVNDP zljbAAdh2%?rh&<+Yu5<>__YC@`+~mojsFEs-4I^1}*nDOg3k6;hy;_s&D@F2v1lQXJ<>;McMLb_HJi_@J8H*M+0?=6m zzPx+C2%=56nY(q(JpW`}ae1j{l3;NSnPPJy4KsrIvoVZ*n-_lqdf)zWjehBeA8jVP zhYhv|MnAN2{7`l1d;)BA&`aFrd7HL5unw;)N{dBjvn#4t-nOop74{dDyKyC6Kdof@ zK7thNrjDNju(;%k;8P=-_Ndx|oHS~KCUf&&vlSCsZH6xeRXz!Z5oZsU_lyt(-8Fqj zN~;gVO^tzH{`J^Z!VePsfrRxfc)qRHyJ$|r24|TP#(^kc-cJ^BnJ891ohMe>OZ|xm zp=wPLUDatB;X7s;atQN;rP{s%eNhgesPTCKnEIjknPK&5;rHvQ1!UlpAhN9Ta5^H7 zW{D1{lH-avqH~7aoP1o-p?onGaR0K?&NQ4)K;I5^)jcJjw(nMm7hZ+&n{lMko=qX0 zxCNPM`vi<${#h2U9$Hjf)x0b}5&d8nXI9sNQxsx*~j5N$1w9Z%35`Iv19Ath}payM!`~@-f#@F@UC{UR0!z?97LtAA{mA?bkZwNoN6> zvO9q&x1_iqVkOJq)WLT5oaMIXgI(VzdoU@pdg_yFQQ1d&q@1hb%DpAV#j>wd_!f9lhfOvE%GnfJhCf=Th~l*yFp8i;--s9 zJD}EJlB5e7Khwww*-?5n=hx{OPk!bb``Z4o>-8)pna%n<+IX^W`C*-(;Iz-fe&&P% zR?;!@)gS0;k8b0y+um$L-oLmmL;I+pard@+`5@MH^s8(+u(ee6`IKn-g7#wNHqmSJ zxxihO?eNIT9}1Y;r<#uDbPI zhk)0v4B1g5{+ywj`y!1Fij%r;$A3Oi36!W~tDCCT^m*}P@n+**E*YnIz{fCZrY1MY zJ7Nl2HxzX)Qvjf&a`pBaD~?D0ZKgQqGm|Hf_b5FQJ2s$cj@dA9;kC81>w@q4Q(%Rq zK6G>@XP(k=XKe@89h&J_Zbg58&Dn6^CK#ZaUgLJ_N-!9e^Shr(-)5S!twu!SAuaU> zIe}<#|5Tpad;QdUE&7d@qIzN?-&qu8;QFN*eU7YKx6MTi`PtuggJVse$I?KqVfU&$ z!S+N(mX^CTeCT6VncT`(xi=`1IKU?1`Z!tTQ>KG#&p$henN#$NInsGQ+$SHoddu5K z-OYV1RplTsgGwq}4}ZjEIXn*jNkW5jmKz=LAeFXGuZ-m(lWR%Qu+nP$lL~%w`s&=N zpMoJfj5}It7OW*zr=fTjxF(pnr(~Z6&2q@h6iKJP34VH6zL`W~2a~3KY7q}meHLVW zmB~p-`HQdHEMFZ=jC4Pq^4z;kz=JNYF=J*yzucm(dwaKoqq)IL&1(6*nB1ij)=Kz$wvl{ zYmd*7tGZ9iySy@T@-Tkv2^98fJQDBjVp9x$l1tsJH8CLaIq}``=g-Me)5?wB)RVAZ5N>f0hgm;7DrRvF{E`{AwRuP5=#k@DRm)OsD`z5Mlr z&cQ3HjcH76?q(G#cR!Qg09=I+`jAL9nOWDj>US41fS4H$(*o`tpy2Aj;)iVTE-#7C zH;&D0-AOVl9EUZq8_;r~DKtc~wJ=EmN3x_IG$&7wZrM+KeU0<@W{Tp<9kH;ZBpRa& z#%?qE9VBTOPZ;-#<$i$IR!X#+%V%7m{Bdi`r|ZlVK3<%S+oTXPK|)B%2#ua>6}Y3c zn)pNQr~RLvwp|@E0xVQg#U5ecoLX*>h9p$s_w{GC#Fb6&u;*;E+Ioma&>I>^tjQR# zd>AXgK^p%MFq0*grN0mHV-Poc-Cs$<xiSig--E_EUaPZF}6f z(ZM5r-|dooCFlL7RErsYBKLEE31lvq&J0 z_6TI}usqYi`C|CA%k@PrX5V<^pz$4V1fJ33{q{Ziuo%63bQrf2un%0 zz)Ckmhc2m-Qc@})-LRCdB1^-9ASFr&NOyhn`{Vm}o|$={XXcG_&pr3t+|~ZWYy+w3 za9trpJk3zNri2Exv4b!f&22>(ZIh&YcAx;5NN4c$u#eP>Xj;*gr8ExpbiytJO#qWJ4d9znhbP>|4Y*RqA{%(-uRIXA)bltB`AV6cjsvqe^5PU7|Il@uuho zIOjW~R6I(lg(ZOclu}TYIerJoeYn^7LgTaD+omJSC2~|$ge~tdQdrgFwcY_T9nP{W zG4{Pyy2kQ&A#q1slkQQ-=4;{s(h8NqR+dE*h4!z>XT(fkPc_t2`Xy9nVMiQ7&*hJ9 zpC#tbNRLB6DnWf>=A z*4i4`~%e_2OH zY#F{ycV0cgU{7!#wJSVxr;={{o@ET9i1+g|h-O9LzEQtT9;g2I)k@`#!cA=J_EPuJ z5`+wJ2eU%khOpvH4>5xOowYJ~fkp1)A+Zybv^gfwTBjpLA~NI7YK3*f!744l+HttD z$_wr*oQqeA#C>k%3R5P30?M!I`dO7T^RK#DV?x+1)IZb@0WiWgShg%Zz3}CjT~3$4 z!f}~=PiEEy^1F-I;<;|*sNl~7%W2FB7Wfm%9b`%N&%luX>Z2AhZQWX-r0Gtq4p+Lo zXZke(%pa81ndWW-N5aTbnBJ8xb4?8v-+MfCr3RkyLAimoxOH?!c}5-HuLWE8m|*$d zaq}im!^073_0Dm@v_?_hM zcYZT`J>b)gDrVQbN)1W)U^a1dDv|T+;NIV5(12AO>lYZ2eAe~dGD;P&kL0#lUU`T) zVTRW3@NIM2hOFRLYdX*+Si9s9tA+XDZ4*79mBGs)FO`~#5BSXBwzDO<7GK@i8ZjLlko6tK zx!D`Ut}96#G|=kC0|TTp=vS+F_w5UPAIYQK2~ccI8o(di(NaAZV8&K9XPFq^HQC&= zet@nRe^5o@Ee=;_t{QPZZT|7@hv}~S8_WZp22yVH1oz*?*gUvycdlf^-Xx`88zoKe zNKpI4lbRi8DZiFE{wWas^1ZT+X=WydboOep*taPAZ##5-(;G;Bx}?&l+epkU zvAoeO1L?Wd19b8Y7)-Ep8(-dUnqYg>%k63fvqvV`x{|t_%jXo5!pN$svACaV>p9Ht z+%q`(R|=Uf^O=^=*FSU)(m3Er*~^59cBqI!DlYsL&QxNTO!Qy+o_>UVVney^XS32Y zUnWyzt_)6W3h~j8P-0cycFAa0(DV1rx%-m~Y8tmPKw!kB!KQP_3oCLTK|QZnHFxks zVKqC?qn-+QMY}hi4YZSvc=$)Bum=?Au0r6PHYkVd$dZ=F$x4=q+n)o(AfE?46F1kD$}&2pf~al6bGsDto}AS}i6>8z z2$?|ULdf!{cyCxh2d}x`zJd-F7Ya8ov>`dW_vrl%8+68Ap$({)@yXgU=$cL&I)XTb zbp%Ewikx$cbhl!qtN1Id&4`j?Tvq0>#XGa)c-9wa^8kPo`A8yJnBV-rZ%M5odD;1w+BIESw$tWKc6&^*A^v{^}1^lCQ)k| zi&Yog3Yq@#Prl7P3cTnVnz@!n$o8QJ#9nf{KiKH~g)hA+V4q4ws zBJcE^K7iiX7_gruW7u^U(pzq_-Zo<6&c>|-Y(pPV!I=sZ6||-9Q-&4jMlsikQ~>EW~$_t*Ej(t2aKrPs_Oi{R1jiZK?nI)LS~@mwf>hXYGpEftSdx{`r?d zjlL0KvNLlYqD@Qwky2wWueq8GU-yK|j^8tSJ+=Pl6D0F&>~I;TBIXx2pIS^WAe4QN zt!gsqh;C7)AI5avjeQ^oFu+Bf2psLaWw!@FFl=$Ga1~4VClP_y0{Mz>B{FhLAS0vf zeE#w%HYD$i%quBzLQTise!zqd2(DY`$0ysPP8ag|NrNX(Bt{%>-VxruD}GWH$LtVx z$eGN5eM71G@(6fJW>4I=QOA0`N=-)JgPP^{?!Y~@(o*}S*$LQLg<2o(qrWVCE5;gR zt*;PaM7kf6byqgF`}QYZb?8UTvPr9@yI{dcJ}B4XjK(b8Atb6D8Y9-hU*vzy<$!Ev zKduA8W@I|>NuURLjoRzLPuhcxW;B zBKOrm2`O1f9q@L9F(L(58}r|@Z{5>0O_X$|(53}UV&^fo9q;~?cNm;b(GoUbIP=nj zEAF4SG<|;F;V)ibgJmUzD+E9u){V2n^rXRFR{RP7Cce*LJ8TtsgK)a+{0?u8t6FY^ zuvUT{UgTK+sz9E3xR!gyj;}Uy>gI`A7vfDr7FMNzzmR1es){eZOPRyu<1eK>iV1Aq z2y6=1{mU9e$k$Bit~lQ*Q{M9xfnZ6&wBj*;Ip+Vunsho<-kbTG(wCB&sz0{>Ud8V7 zCLJVKXLh+TMO3S7GczBLr-_wOo2Ra;BoksQxp@XlfvGlbe z#xX$HLY!RaOUhzMM6mR7Nnc3*dn|2ICC(o!HDiW5mK1v$!V?^tp~lKL#XfkIr&Aqg z$kUgKM;CP|4_MP8k?92@mZk-B;$l4IpckXUUj>jyH;V4hK2v(?%0ZTfX8ns+5X5x8Y0UD> zTzTw3)PdliKNelSD|iA$9Uo;<^vj(C4;XYmw^}k~;FBJuDe1fC0IX#~Q`=xQoH8tH z(p^rC^c*63KD5Sdp8hFCGd=|v=DqWq?&LOgEW}hQiT2Ytbjid#37WB&wvRG9lPu-D zakvR3z>AB*wWwUx?LESQmjhg3*!0BZl}dD)Qc&ul^yb#v8#hKHdu!V&kh=u`XP

vT0#N(P1VjP^vyI+2m^BL2J$XmanrHB7IOHYrv69b#O;VF#5`vuj^ozJ zj9I4K^tbLPU#kJ1-t{5Lc6hKo@{cuovpj|~f>(TiJqUw-lV%~rS_o0%+ca{BJ3XEF z9CQ#1^-LSK4`;h7$O2C14Un%S)e|5r*2EAxuJa0!qY^;;XGBjT^sIegQ1@1T~?sUw;Ae~Qpp-7bNdHqq|`Fvpd z;j;)6gFdeacaeYy^Cf)vNy#+(xuVBRxK=#H{C=9c8ttVJ=@=V>BG^^r+VBj~fq7B% zI|c0&%psi<5MjKVD}Thgq8azh4ajnLNa8_t+1;U1fe30(=}C$Ns2OHL6!|NS(XYGq ziQZrF-NbfJwG-CYPwZ3JuBal7u4A081qm+{DRjikS$Rh@eO3lCZ01Sb4t#pmttH>$ z*go91txy0c7+s_o-y2i&PP=1yiDJy>=Zhb6Pb@V9RW58`NTBc%c~eL0Tq4H?Ga|Z zItvHt>0lj!d7dS^1Y0dKhMl(I-2~|Q?yHJ?IcY*b&oO2vwB&@Pbmdo?q>GehF!DS7 z*kE9I7twkCljXz`f28f>^!ox*q91BLT%z)hLBv*g!oa?S1kScBYr`H>8brID5M_w* z-dliy&0rTCDeGS`M=Zaufv3KgE0rsD8v`tw?JP2P!Y<`ji0|1`&kM zp1EZxasJ;gRS0yz8~KVm%aFAq`^L8i3Zzmhs;hJYdv~%O#W^kK(-qpY|K5U7A@f~T zmxiJ3tGW8^(&=w3=U=qo5U3K%XTT0to;`@sw!UJ7Sg2?(wz^bknVzHDvTc<=4_Ko` zH8UtHHIHz1Ot%V1JB`s+N#@_CmD>A7QaLR#_V0}S`SB+jj?qY1F(i)5n?&U``4U8= zm^AJ{Xrmdjahy_g>%L*~ux-&r?#*mF#1Ue(e_D22=|CU^JSPmqQk^hcw{R4L4#Zjy zNjrPB1A@uCDI@078e+6`YZ;EafHZvh>tmLR&|mAS3}Dx+2wxq%Y4LD!<+m+<{+5Oi zQ!*QedQ#b-(&NdeYD|2N)aS^lxfEk5JG6}3nkj<7dlD5of;s`qFT-PgcVeTrnfvx* z-(N0{I$-2$3!g$qN>Ym4rRW_86sK?1XH8scG{_^SY1+R}X)^ek^*t#7)UR9`a2&jP zQ6)n4CCJ*Xn{{j~mxAr?{}5FExXdmCf4XbesND3l?>^$`Ve96HXNv1bsyYxs#6uEO zq8#ASjT3T>*3Y0Pnd>{1y~4iwyhb+XR&FBj7u)GF*29gTBy=#6t<8wO=D&qC^xiU| zZL zd4Lh`zM)63h;ej4(JUux`fFb~(GAuA@)P_b_m>pZtPjx8`GzL^koX6vQm1cCA%vr+ z{K%=tmQs$t|5^Ly;cuFJdpws~lw-o9;_4xTJqY=2+9g#g(0|Q&^{*Cp!^ykUX%bu^ zTB+Fxq+%C}Q_eZVi5WDi89Z{l?^#PsTbS6M*a$~cs9xM~eUJ`s;QK|HCttQ&nH77L zU@N#(#b;$n#Ma-0gGM~+nx-$HE~(P^V!ie59kL#VQ+N`a3JD}eE2O+8VWe2MsO&nU zd_p%d7j=?`yHtR_ z@O_&!<=KLi`3RNtUwNH8Y!bgK9_C;KrzAcHK_CV{>1h-q-&ml@w?@`G-C}>EE`D`# z)_+sdQU5ZLx%*>lN#;$O%t+>g?V}snZY1T4 zbB*4JrU}L;35T~Xc~c_*Epec80r`EhpjW*#PoQ*n>-yqV8)G0yPsuuHW>9-^uJH@36;S+s zX+sqC_yw(rn~uu}!#QUdl{gzJvnEO)g}w*q&;TeE+TCNq93Ht{2-w0xDVCKAfr$lM z4&IoC#|+)d&x@VJaepk&%LpM>_KmCb%fE=MI0ASqCQ%Z_xtQMfD1+{}O@3a&39O8Z zx4a%p+=mbDG2RV~a)w7;n6;a3OS07xM**q^9LyrK+a@mFhW9dsc6;TsjAoV$qc>!E zwZC>4w=-n?QdiSbklsF$y`td+&c$0n`QR#F-|m1jggpA-RvEdR?}xEimekUESAY=n z#aBL`X|1ipDra(lnd%)_3-?@~6cwHRYC1w6RqT~u9zSSsYc9o;iD+zE65X&jh4gLm zu-g_3Rk;O0J>;leNTI6z!%05ad$yj7-E)1XnwIJrpf&iJ{2fF@b}W7^UAQpM@jYNKA9%U&Gy zWyv%dR8>ULf1h0su`38t{4k24>ehmFl}dkb&dmzh`10pw>H57t3Fi;Q+yjii z7^~=!(}UWdK^oci%kD&cqa)Vmv9jJ3BlyecFYwFsWa!l!yqz;kkSxhj#_2(5W)Zcu zdpqZyt;Bs@DyuHnv4c|LGMgDsonk*H6qMWN()?A>b8Le_JEz*6wdsnvnL(LZu)k6? zu*P_+u38|b|UV}t{tHD?<}aO{s+DHu@Razc;Ib< zSVx-in(>WvKd8-m0q0y^nKB8-m3pv$!CO%p?d!6ES64%Uuy`^|c;xE|Z>QZ4b90`29>?6nWSdjRPnpT9EI5KmnM=&N`MB}#Iy1I z=-8?QwSUFdO@GlPgBGJRS0H-H6xB*kt+JZ@qM$#2A7oguLs|D${@BTswcNX-dKpNc zw)`7c*dz!Gd=#-wnjg&-Fk0&W=pzQ^uK$9B$eN7IgShc%e(0fHy;E+&u1#=iAN7nxXQ)8bAC?>_bUoI}DfOqg`vUw`SR z^2vO$FT)y?v5OCKqARks9xfkxgoL#iDwq1GbIr^}d;iqSIqxw2=&RV6r+J;cvwZzZ zIm7%lFG@`7!U7xjMMnVCRP5-mziHw^<*7SvmHBf=DMTi%{S&5D1zd+%ZdAi$D|4&9Kpbw=lH%tXor$nqUhHWxgV| z5+$h?YTDfgC1sJI_;Jv6CBCp?`b}gqZIZHUiSK>A#|qi8kH0`UuaiHK$PBZ~>AN(= z>EYf-d7J)Mb4Y^$|G%k1ygPH)Z}Ujlb#mG9HxFH_8ve5`InD$qo1tH#esF+7pYWmn zz&Jf{`878?-c)Kj0>|2x9w?l88ua~Tuk^!53m>h}^&^UYUJquS;$+D85lPEKN> diff --git a/e2e/work-orders/work-orders.visual.spec.ts b/e2e/work-orders/work-orders.visual.spec.ts index 0816018f..547723a6 100644 --- a/e2e/work-orders/work-orders.visual.spec.ts +++ b/e2e/work-orders/work-orders.visual.spec.ts @@ -82,6 +82,10 @@ function isBoardDetail(pathname: string) { return /\/workorders\/\d+\/detail$/.test(pathname); } +function isAccountList(pathname: string) { + return pathname.includes("/Account/GetAccountList"); +} + async function mockWorkOrderApi(page: Page, mode: "default" | "empty" | "error" = "default") { await page.clock.setFixedTime(new Date(FROZEN_NOW)); await page.addInitScript((user) => localStorage.setItem("auth", JSON.stringify(user)), TEST_USER); @@ -161,6 +165,27 @@ async function mockWorkOrderApi(page: Page, mode: "default" | "empty" | "error" return; } + if (isAccountList(pathname)) { + await fulfillJson(route, { + items: [ + { + id: 1, + name: "Amazon", + number: "AMZ-1", + industry: "Retail", + website: "", + primaryContact: "", + phoneNumber: "", + owner: "", + }, + ], + totalCount: 1, + page: 1, + pageSize: 50, + }); + return; + } + await fulfillJson(route, { data: [] }); }); } diff --git a/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-account-field.ts b/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-account-field.ts new file mode 100644 index 00000000..4664c494 --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-account-field.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from "react"; + +type UseSiteDialogAccountFieldArgs = { + open: boolean; + customer: string; + createMode: boolean; + dirty: boolean; + onDirty: () => void; +}; + +export function useSiteDialogAccountField({ + open, + customer, + createMode, + dirty, + onDirty, +}: UseSiteDialogAccountFieldArgs) { + const [accountName, setAccountNameState] = useState(customer); + + useEffect(() => { + if (!open || dirty) { + return; + } + setAccountNameState(customer); + }, [open, dirty, customer]); + + return { + accountName, + accountMissing: createMode && !accountName.trim(), + customerPatch: createMode ? { customer: accountName } : {}, + setAccountName: (next: string) => { + onDirty(); + setAccountNameState(next); + }, + }; +} diff --git a/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-form-fields.ts b/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-form-fields.ts new file mode 100644 index 00000000..1b0a4f30 --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-form-fields.ts @@ -0,0 +1,122 @@ +import { useEffect, useState } from "react"; +import { resolveLocationId } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-helpers"; +import { resolveSiteCode } from "@/domain/work-orders/mappers/work-order-board-mapper"; +import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact"; +import type { LocationOption } from "@/domain/work-orders/types/work-order"; + +type UseSiteDialogFormFieldsArgs = { + open: boolean; + value: string; + locationId: string | number; + pocName: string; + pocPhone: string; + pocNotes: string; + additionalContacts: WorkOrderAdditionalContact[]; + sites: LocationOption[]; + createMode: boolean; +}; + +export function useSiteDialogFormFields({ + open, + value, + locationId, + pocName, + pocPhone, + pocNotes, + additionalContacts, + sites, + createMode, +}: UseSiteDialogFormFieldsArgs) { + const [code, setCode] = useState(value); + const [locId, setLocId] = useState(() => resolveLocationId(locationId, value, sites)); + const [pn, setPn] = useState(pocName); + const [pp, setPp] = useState(pocPhone); + const [notes, setNotes] = useState(pocNotes); + const [extraContacts, setExtraContacts] = useState(additionalContacts); + const [pocFilledFor, setPocFilledFor] = useState(""); + const [dirty, setDirty] = useState(false); + const [contactsDirty, setContactsDirty] = useState(false); + const [baselineHadContacts, setBaselineHadContacts] = useState( + () => additionalContacts.length > 0, + ); + const [showErrors, setShowErrors] = useState(false); + + useEffect(() => { + if (!open) { + setDirty(false); + setContactsDirty(false); + setShowErrors(false); + } + }, [open]); + + useEffect(() => { + if (!open || dirty) { + return; + } + const resolved = resolveLocationId(locationId, value, sites); + setCode(value); + setLocId(resolved); + setPn(pocName); + setPp(pocPhone); + setNotes(pocNotes); + setExtraContacts(additionalContacts); + setBaselineHadContacts(additionalContacts.length > 0); + setPocFilledFor(pocName.trim() || pocPhone.trim() ? resolved : ""); + }, [open, dirty, value, locationId, pocName, pocPhone, pocNotes, additionalContacts, sites]); + + const handlePick = (id: string) => { + const next = sites.find((s) => String(s.id) === id); + const siteChanged = id !== locId; + setLocId(id); + if (next) { + setCode(resolveSiteCode(next)); + } + if (siteChanged) { + setExtraContacts([]); + setContactsDirty(true); + setDirty(true); + if (createMode) { + setPn(""); + setPp(""); + } + setPocFilledFor(""); + } else if (id !== pocFilledFor) { + setPocFilledFor(""); + } + }; + + const markPocDirty = (apply: () => void) => { + setDirty(true); + apply(); + }; + + return { + code, + locId, + pn, + pp, + notes, + extraContacts, + pocFilledFor, + dirty, + contactsDirty, + baselineHadContacts, + showErrors, + selected: sites.find((s) => String(s.id) === locId), + siteMissing: !locId, + setPocFilledFor, + setPn, + setPp, + setShowErrors, + markDirty: () => setDirty(true), + handlePick, + setPnDirty: (next: string) => markPocDirty(() => setPn(next)), + setPpDirty: (next: string) => markPocDirty(() => setPp(next)), + setNotesDirty: (next: string) => markPocDirty(() => setNotes(next)), + setExtraContactsDirty: (contacts: WorkOrderAdditionalContact[]) => { + setDirty(true); + setContactsDirty(true); + setExtraContacts(contacts); + }, + }; +} diff --git a/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts b/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts index afd70e08..5259e328 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts +++ b/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts @@ -1,10 +1,10 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { buildSiteDialogPatch } from "@/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch"; -import { resolveLocationId } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-helpers"; import type { SitePatch } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-types"; +import { useSiteDialogAccountField } from "@/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-account-field"; +import { useSiteDialogFormFields } from "@/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-form-fields"; import { useLocationDetail } from "@/domain/locations/use-cases/use-location-detail"; import { formatLocationAddressPreview } from "@/domain/locations/mappers/location-mapper"; -import { resolveSiteCode } from "@/domain/work-orders/mappers/work-order-board-mapper"; import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact"; import type { LocationOption } from "@/domain/work-orders/types/work-order"; @@ -39,69 +39,36 @@ export function useSiteDialogState({ createMode, onSave, }: UseSiteDialogStateArgs) { - const [code, setCode] = useState(value); - const [accountName, setAccountName] = useState(customer); - const [locId, setLocId] = useState(() => resolveLocationId(locationId, value, sites)); - const [pn, setPn] = useState(pocName); - const [pp, setPp] = useState(pocPhone); - const [notes, setNotes] = useState(pocNotes); - const [extraContacts, setExtraContacts] = useState(additionalContacts); - const [pocFilledFor, setPocFilledFor] = useState(""); - const [dirty, setDirty] = useState(false); - const [contactsDirty, setContactsDirty] = useState(false); - const [baselineHadContacts, setBaselineHadContacts] = useState( - () => additionalContacts.length > 0, - ); - const [showErrors, setShowErrors] = useState(false); - + const fields = useSiteDialogFormFields({ + open, + value, + locationId, + pocName, + pocPhone, + pocNotes, + additionalContacts, + sites, + createMode, + }); + const account = useSiteDialogAccountField({ + open, + customer, + createMode, + dirty: fields.dirty, + onDirty: fields.markDirty, + }); + const { locId, pocFilledFor, pn, pp, setPn, setPp, setPocFilledFor, siteMissing } = fields; + const { accountMissing } = account; const { data: locationDetail, isLoading: locationDetailLoading, isError: locationDetailError, } = useLocationDetail(open && locId ? locId : undefined); - const selected = sites.find((s) => String(s.id) === locId); const addressPreview = locationDetail ? formatLocationAddressPreview(locationDetail) : ""; - const siteMissing = !locId; - const accountMissing = createMode && !accountName.trim(); const pocMissing = !pn.trim() || !pp.trim(); const canConfirm = !siteMissing && (!createMode || (!pocMissing && !accountMissing)); - useEffect(() => { - if (!open) { - setDirty(false); - setContactsDirty(false); - setShowErrors(false); - } - }, [open]); - - useEffect(() => { - if (!open || dirty) { - return; - } - const resolved = resolveLocationId(locationId, value, sites); - setCode(value); - setAccountName(customer); - setLocId(resolved); - setPn(pocName); - setPp(pocPhone); - setNotes(pocNotes); - setExtraContacts(additionalContacts); - setBaselineHadContacts(additionalContacts.length > 0); - setPocFilledFor(pocName.trim() || pocPhone.trim() ? resolved : ""); - }, [ - open, - dirty, - value, - locationId, - customer, - pocName, - pocPhone, - pocNotes, - additionalContacts, - sites, - ]); - useEffect(() => { if (!open || !locId || !locationDetail || pocFilledFor === locId) { return; @@ -109,89 +76,53 @@ export function useSiteDialogState({ setPn((prev) => (prev.trim() ? prev : (locationDetail.contact ?? ""))); setPp((prev) => (prev.trim() ? prev : (locationDetail.phone ?? ""))); setPocFilledFor(locId); - }, [open, locId, locationDetail, pocFilledFor]); - - const handlePick = (id: string) => { - const next = sites.find((s) => String(s.id) === id); - const siteChanged = id !== locId; - setLocId(id); - if (next) { - setCode(resolveSiteCode(next)); - } - if (siteChanged) { - setExtraContacts([]); - setContactsDirty(true); - setDirty(true); - if (createMode) { - setPn(""); - setPp(""); - } - setPocFilledFor(""); - } else if (id !== pocFilledFor) { - setPocFilledFor(""); - } - }; - - const buildPatch = (): SitePatch => - buildSiteDialogPatch({ - code, - locId, - selected, - ...(createMode ? { customer: accountName } : {}), - pocName: pn, - pocPhone: pp, - pocNotes: notes, - extraContacts, - contactsDirty, - baselineHadContacts, - }); - - const markPocDirty = (apply: () => void) => { - setDirty(true); - apply(); - }; - - const markContactsDirty = (contacts: WorkOrderAdditionalContact[]) => { - setDirty(true); - setContactsDirty(true); - setExtraContacts(contacts); - }; + }, [open, locId, locationDetail, pocFilledFor, setPn, setPp, setPocFilledFor]); const attemptSave = () => { if (createMode && !canConfirm) { - setShowErrors(true); + fields.setShowErrors(true); return; } - onSave(buildPatch()); + onSave( + buildSiteDialogPatch({ + code: fields.code, + locId: fields.locId, + selected: fields.selected, + ...account.customerPatch, + pocName: fields.pn, + pocPhone: fields.pp, + pocNotes: fields.notes, + extraContacts: fields.extraContacts, + contactsDirty: fields.contactsDirty, + baselineHadContacts: fields.baselineHadContacts, + }), + ); onOpenChange(false); }; return { - locId, - code, - accountName, - accountMissing, - selected, + locId: fields.locId, + code: fields.code, + accountName: account.accountName, + accountMissing: account.accountMissing, + selected: fields.selected, locationDetail, locationDetailLoading, locationDetailError, addressPreview, - pn, - pp, - notes, - extraContacts, - showErrors, - siteMissing, - handlePick, - setAccountName: (next: string) => { - setDirty(true); - setAccountName(next); - }, + pn: fields.pn, + pp: fields.pp, + notes: fields.notes, + extraContacts: fields.extraContacts, + showErrors: fields.showErrors, + siteMissing: fields.siteMissing, + handlePick: fields.handlePick, + setAccountName: account.setAccountName, attemptSave, - setPn: (next: string) => markPocDirty(() => setPn(next)), - setPp: (next: string) => markPocDirty(() => setPp(next)), - setNotes: (next: string) => markPocDirty(() => setNotes(next)), - setExtraContacts: markContactsDirty, + setPn: fields.setPnDirty, + setPp: fields.setPpDirty, + setNotes: fields.setNotesDirty, + setExtraContacts: fields.setExtraContactsDirty, close: () => onOpenChange(false), }; } diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-field-select.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-field-select.tsx index ad53384d..ede43f70 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-field-select.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-field-select.tsx @@ -17,6 +17,7 @@ type WizardFieldSelectProps = { search?: boolean; searchPlaceholder?: string; disabled?: boolean; + onSearchChange?: (query: string) => void; }; function filterOptions(options: WizardMenuOption[], query: string): WizardMenuOption[] { @@ -41,15 +42,22 @@ export function WizardFieldSelect({ search = true, searchPlaceholder = "Search…", disabled = false, + onSearchChange, }: WizardFieldSelectProps) { const { anchorEl, open, setAnchor, close } = useMenuAnchor(); const [q, setQ] = useState(""); const selected = options.find((o) => o.key === value); - const filtered = filterOptions(options, q); + const filtered = onSearchChange ? options : filterOptions(options, q); + + const handleQueryChange = (next: string) => { + setQ(next); + onSearchChange?.(next); + }; const handleClose = () => { close(); setQ(""); + onSearchChange?.(""); }; return ( @@ -117,7 +125,7 @@ export function WizardFieldSelect({ search={search} searchPlaceholder={searchPlaceholder} query={q} - onQueryChange={setQ} + onQueryChange={handleQueryChange} options={filtered} value={value} onPick={(key) => { diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-footer.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-footer.tsx index 6f01be6a..36a4e4ba 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-footer.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-footer.tsx @@ -3,10 +3,10 @@ import { Button } from "@/components/ui/button"; import { WIZARD_STEPS, canContinueWizardStep1, - canContinueWizardStep2, canCreateWorkOrderFromWizard, isWizardStepValid, isWizardWoNumberValid, + wizardStep2ContinueHint, type WorkOrderWizardDraft, } from "@/domain/work-orders/types/work-order-wizard"; @@ -28,9 +28,8 @@ function stepHint(step: number, draft: WorkOrderWizardDraft): string | null { } return "Select severity to continue"; } - if (step === 2 && !canContinueWizardStep2(draft)) { - if (!draft.locationId) return "Pick a site to continue"; - return "Enter POC name and phone to continue"; + if (step === 2) { + return wizardStep2ContinueHint(draft); } return null; } diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select.tsx index 5ab5a3e1..b3e7e444 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select.tsx @@ -1,8 +1,10 @@ +import { useState } from "react"; import { Building2, Info } from "lucide-react"; import { Text } from "@/components/ui/text"; import { WizardFieldSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-field-select"; import { WizLabel } from "@/app/(protected)/workorders/_components/wizard/wizard-labels"; import { useAccountsList } from "@/domain/accounts/use-cases/use-accounts-list"; +import { useDebounce } from "@/hooks/use-debounce"; type WizardStepLocationAccountSelectProps = { customer: string; @@ -10,17 +12,21 @@ type WizardStepLocationAccountSelectProps = { showError?: boolean; }; +function accountOptions(names: string[], selected: string) { + const withSelected = selected && !names.includes(selected) ? [selected, ...names] : names; + return withSelected.map((name) => ({ key: name, label: name })); +} + export function WizardStepLocationAccountSelect({ customer, onCustomerChange, showError = false, }: WizardStepLocationAccountSelectProps) { - const { data } = useAccountsList({ page: 1, pageSize: 100, search: "" }); - const accounts = data?.items ?? []; - const options = accounts - .map((account) => account.name.trim()) - .filter(Boolean) - .map((name) => ({ key: name, label: name })); + const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search, 300); + const { data } = useAccountsList({ page: 1, pageSize: 50, search: debouncedSearch }); + const names = (data?.items ?? []).map((account) => account.name.trim()).filter(Boolean); + const selected = customer.trim(); return (
@@ -32,8 +38,9 @@ export function WizardStepLocationAccountSelect({ } searchPlaceholder="Search account name…" - options={options} + options={accountOptions(names, selected)} onPick={onCustomerChange} + onSearchChange={setSearch} /> ({ - useAccountsList: () => ({ - data: { - items: [ - { - id: 1, - name: "Amazon", - number: "", - industry: "", - website: "", - primaryContact: "", - phoneNumber: "", - owner: "", - }, - { - id: 2, - name: "Sea Haven", - number: "", - industry: "", - website: "", - primaryContact: "", - phoneNumber: "", - owner: "", - }, - ], - }, - }), +const { useAccountsListMock } = vi.hoisted(() => ({ + useAccountsListMock: vi.fn(), })); +vi.mock("@/hooks/use-debounce", () => ({ + useDebounce: (value: string) => value, +})); + +vi.mock("@/domain/accounts/use-cases/use-accounts-list", () => ({ + useAccountsList: (params: unknown) => useAccountsListMock(params), +})); + +const ACCOUNTS = { + items: [ + { + id: 1, + name: "Amazon", + number: "", + industry: "", + website: "", + primaryContact: "", + phoneNumber: "", + owner: "", + }, + { + id: 2, + name: "Sea Haven", + number: "", + industry: "", + website: "", + primaryContact: "", + phoneNumber: "", + owner: "", + }, + ], +}; + describe("WizardStepLocationAccountSelect", () => { + beforeEach(() => { + useAccountsListMock.mockReturnValue({ data: ACCOUNTS }); + }); + it("sets draft.customer to the exact account name on pick", () => { const onCustomerChange = vi.fn(); renderWithProviders( @@ -45,4 +57,20 @@ describe("WizardStepLocationAccountSelect", () => { expect(onCustomerChange).toHaveBeenCalledWith("Amazon"); }); + + it("passes typed search to the accounts list query", () => { + renderWithProviders( + , + { withAuth: false }, + ); + + expect(useAccountsListMock).toHaveBeenCalledWith({ page: 1, pageSize: 50, search: "" }); + + fireEvent.click(screen.getByRole("button", { name: /select account/i })); + fireEvent.change(screen.getByPlaceholderText("Search account name…"), { + target: { value: "Sea" }, + }); + + expect(useAccountsListMock).toHaveBeenCalledWith({ page: 1, pageSize: 50, search: "Sea" }); + }); }); diff --git a/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts b/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts index fe70bd74..17c488f0 100644 --- a/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts +++ b/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts @@ -7,6 +7,7 @@ import { canCreateWorkOrderFromWizard, isWizardStepValid, isWizardWoNumberValid, + wizardStep2ContinueHint, } from "@/domain/work-orders/types/work-order-wizard"; describe("mapWizardDraftToFormValues", () => { @@ -135,6 +136,22 @@ describe("wizard step validation", () => { }; expect(canContinueWizardStep2(draft)).toBe(false); expect(isWizardStepValid(2, draft)).toBe(false); + expect(wizardStep2ContinueHint(draft)).toBe("Select an account to continue"); + }); + + it("hints pick a site when step 2 has no location", () => { + expect(wizardStep2ContinueHint(EMPTY_WIZARD_DRAFT)).toBe("Pick a site to continue"); + }); + + it("hints POC when step 2 has site and account but no phone", () => { + const draft = { + ...EMPTY_WIZARD_DRAFT, + locationId: "99", + siteCode: "BK5", + customer: "Amazon", + pocName: "Jordan", + }; + expect(wizardStep2ContinueHint(draft)).toBe("Enter POC name and phone to continue"); }); it("allows step 2 when site, account, and POC are set", () => { @@ -150,6 +167,7 @@ describe("wizard step validation", () => { }; expect(canContinueWizardStep2(draft)).toBe(true); expect(isWizardStepValid(2, draft)).toBe(true); + expect(wizardStep2ContinueHint(draft)).toBeNull(); }); it("blocks create without siteCode", () => { From b240cc8c793f7a49596e823a42bb4713c58e89c6 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Tue, 25 Aug 2026 21:10:12 -0300 Subject: [PATCH 3/4] fix(work-orders): gate account picker to org-wide scope Hide GetAccountList and skip client customer on create unless org_scope is all, so scoped users cannot read another tenant name. --- .../list/table/cells/site-dialog.tsx | 8 +- .../cells/use-site-dialog-account-field.ts | 6 +- .../list/table/cells/use-site-dialog-state.ts | 3 + .../list/table/wo-table-inline-row.tsx | 8 +- .../_components/wizard/wizard-footer.tsx | 19 +++-- .../wizard-step-location-account-select.tsx | 11 ++- .../accounts/use-cases/use-accounts-list.ts | 2 + src/domain/auth/types/auth-user.ts | 3 + .../work-orders/types/work-order-wizard.ts | 42 +++++++--- .../utils/get-inline-create-missing.ts | 11 ++- src/lib/auth/org-scope.ts | 45 ++++++++++ ...zard-step-location-account-select.test.tsx | 83 ++++++++++++++++--- ...work-order-slide-over-failed-save.test.tsx | 10 ++- ...ork-orders-advanced-search-wiring.test.tsx | 10 ++- ...ork-orders-board-core-affordances.test.tsx | 10 ++- .../work-orders-dispatcher-layout.test.tsx | 10 ++- .../mappers/wizard-draft-mapper.test.ts | 15 ++++ .../utils/get-inline-create-missing.test.ts | 15 ++++ src/test/lib/auth/org-scope.test.ts | 51 ++++++++++++ 19 files changed, 313 insertions(+), 49 deletions(-) create mode 100644 src/lib/auth/org-scope.ts create mode 100644 src/test/lib/auth/org-scope.test.ts diff --git a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx index ecf6e3ba..e68dc5c7 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog.tsx @@ -1,4 +1,4 @@ -import type { MouseEvent } from "react"; +import { useContext, type MouseEvent } from "react"; import { MapPin } from "lucide-react"; import { Dialog, DialogContent, DialogTitle } from "@mui/material"; import { WizardFieldSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-field-select"; @@ -13,6 +13,8 @@ import { WizardStepLocationAccountSelect } from "@/app/(protected)/workorders/_c import { SiteDialogPocSection } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-poc-section"; import type { SitePatch } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-types"; import { useSiteDialogState } from "@/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state"; +import { isOrgWideScope } from "@/lib/auth/org-scope"; +import { AuthContext } from "@/providers/auth-context"; export type { SitePatch }; @@ -47,6 +49,7 @@ export function SiteDialog({ viewOnly = false, onSave, }: SiteDialogProps) { + const requireAccount = isOrgWideScope(useContext(AuthContext)?.user); const s = useSiteDialogState({ open, onOpenChange, @@ -59,6 +62,7 @@ export function SiteDialog({ additionalContacts, sites, createMode, + requireAccount, onSave, }); @@ -85,7 +89,7 @@ export function SiteDialog({
- {createMode && ( + {Boolean(createMode && requireAccount) && ( void; }; @@ -12,6 +13,7 @@ export function useSiteDialogAccountField({ open, customer, createMode, + requireAccount = true, dirty, onDirty, }: UseSiteDialogAccountFieldArgs) { @@ -26,8 +28,8 @@ export function useSiteDialogAccountField({ return { accountName, - accountMissing: createMode && !accountName.trim(), - customerPatch: createMode ? { customer: accountName } : {}, + accountMissing: createMode && requireAccount && !accountName.trim(), + customerPatch: createMode && requireAccount ? { customer: accountName } : {}, setAccountName: (next: string) => { onDirty(); setAccountNameState(next); diff --git a/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts b/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts index 5259e328..ca321241 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts +++ b/src/app/(protected)/workorders/_components/list/table/cells/use-site-dialog-state.ts @@ -22,6 +22,7 @@ type UseSiteDialogStateArgs = { additionalContacts: WorkOrderAdditionalContact[]; sites: LocationOption[]; createMode: boolean; + requireAccount?: boolean; onSave: (patch: SitePatch) => void; }; @@ -37,6 +38,7 @@ export function useSiteDialogState({ additionalContacts = [], sites, createMode, + requireAccount = true, onSave, }: UseSiteDialogStateArgs) { const fields = useSiteDialogFormFields({ @@ -54,6 +56,7 @@ export function useSiteDialogState({ open, customer, createMode, + requireAccount, dirty: fields.dirty, onDirty: fields.markDirty, }); diff --git a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx index 94431d21..9a0d3a8a 100644 --- a/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx +++ b/src/app/(protected)/workorders/_components/list/table/wo-table-inline-row.tsx @@ -1,4 +1,4 @@ -import { useRef, useState, type RefObject } from "react"; +import { useContext, useRef, useState, type RefObject } from "react"; import { Flag } from "lucide-react"; import { WorkOrderCompleteDialog } from "@/app/(protected)/workorders/_components/list/work-order-complete-dialog"; @@ -21,6 +21,8 @@ import { inlineCreateSaveHint, inlineCreateSiteError, } from "@/domain/work-orders/utils/get-inline-create-missing"; +import { isOrgWideScope } from "@/lib/auth/org-scope"; +import { AuthContext } from "@/providers/auth-context"; import { WO_TABLE_ROW_HEIGHT } from "./wo-table-cols"; import { WoTableInlineRowCells } from "./wo-table-inline-row-cells"; @@ -71,7 +73,9 @@ export function WoTableInlineRow({ onDuplicateFound, }: WoTableInlineRowProps) { const weekDefault = draftDayKey === UNSCHEDULED_DAY_KEY || draftDayKey === UNASSIGNED_DAY_KEY; - const missing = getInlineCreateMissing(draft); + const missing = getInlineCreateMissing(draft, { + requireAccount: isOrgWideScope(useContext(AuthContext)?.user), + }); const canSave = !saving && missing.length === 0; const [showErrors, setShowErrors] = useState(false); const [confirmComplete, setConfirmComplete] = useState(false); diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-footer.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-footer.tsx index 36a4e4ba..9d6a2207 100644 --- a/src/app/(protected)/workorders/_components/wizard/wizard-footer.tsx +++ b/src/app/(protected)/workorders/_components/wizard/wizard-footer.tsx @@ -1,3 +1,4 @@ +import { useContext } from "react"; import { Check, ChevronLeft, ChevronRight, Lightbulb } from "lucide-react"; import { Button } from "@/components/ui/button"; import { @@ -7,8 +8,11 @@ import { isWizardStepValid, isWizardWoNumberValid, wizardStep2ContinueHint, + type WizardAccountGate, type WorkOrderWizardDraft, } from "@/domain/work-orders/types/work-order-wizard"; +import { isOrgWideScope } from "@/lib/auth/org-scope"; +import { AuthContext } from "@/providers/auth-context"; type WizardFooterProps = { step: number; @@ -21,7 +25,11 @@ type WizardFooterProps = { isCreating: boolean; }; -function stepHint(step: number, draft: WorkOrderWizardDraft): string | null { +function stepHint( + step: number, + draft: WorkOrderWizardDraft, + gate: WizardAccountGate, +): string | null { if (step === 1 && !canContinueWizardStep1(draft)) { if (!isWizardWoNumberValid(draft.woNumber)) { return "Enter an alphanumeric WO number, or leave blank for SH generation"; @@ -29,7 +37,7 @@ function stepHint(step: number, draft: WorkOrderWizardDraft): string | null { return "Select severity to continue"; } if (step === 2) { - return wizardStep2ContinueHint(draft); + return wizardStep2ContinueHint(draft, gate); } return null; } @@ -44,9 +52,10 @@ export function WizardFooter({ onAttemptContinue, isCreating, }: WizardFooterProps) { - const stepValid = isWizardStepValid(step, draft); - const canCreate = canCreateWorkOrderFromWizard(draft); - const hint = stepHint(step, draft); + const accountGate = { requireAccount: isOrgWideScope(useContext(AuthContext)?.user) }; + const stepValid = isWizardStepValid(step, draft, accountGate); + const canCreate = canCreateWorkOrderFromWizard(draft, accountGate); + const hint = stepHint(step, draft, accountGate); return (
account.name.trim()).filter(Boolean); const selected = customer.trim(); + if (!orgWide) { + return null; + } + return (
Account diff --git a/src/domain/accounts/use-cases/use-accounts-list.ts b/src/domain/accounts/use-cases/use-accounts-list.ts index 9dcb347a..2a031a0c 100644 --- a/src/domain/accounts/use-cases/use-accounts-list.ts +++ b/src/domain/accounts/use-cases/use-accounts-list.ts @@ -5,10 +5,12 @@ import { queryKeys } from "@/infra/query-key/query-key"; export function useAccountsList( params: AccountsListParams, + enabled = true, ): UseQueryResult { return useQuery({ queryKey: queryKeys.accounts.list(params as Record), queryFn: () => accountsApi.getList(params), placeholderData: keepPreviousData, + enabled, }); } diff --git a/src/domain/auth/types/auth-user.ts b/src/domain/auth/types/auth-user.ts index 6c5faf2a..f16f38b0 100644 --- a/src/domain/auth/types/auth-user.ts +++ b/src/domain/auth/types/auth-user.ts @@ -7,6 +7,9 @@ export interface AuthUser { fullname: string; id: string | number; name?: string; + /** JWT / login `org_scope`. Only `all` may see org-wide account lists. */ + orgScope?: string; + accountId?: string | number; } export interface LoginCredentials { diff --git a/src/domain/work-orders/types/work-order-wizard.ts b/src/domain/work-orders/types/work-order-wizard.ts index d9573867..20a53a92 100644 --- a/src/domain/work-orders/types/work-order-wizard.ts +++ b/src/domain/work-orders/types/work-order-wizard.ts @@ -197,34 +197,56 @@ export function canContinueWizardStep1(draft: WorkOrderWizardDraft): boolean { return true; } -export function canContinueWizardStep2(draft: WorkOrderWizardDraft): boolean { - const hasSite = Boolean(draft.locationId && (draft.siteCode.trim() || draft.locationName.trim())); - return Boolean(hasSite && draft.customer.trim() && draft.pocName.trim() && draft.pocPhone.trim()); +export type WizardAccountGate = { + requireAccount?: boolean; +}; + +function requiresAccount(options?: WizardAccountGate): boolean { + return options?.requireAccount !== false; } -export function wizardStep2ContinueHint(draft: WorkOrderWizardDraft): string | null { - if (canContinueWizardStep2(draft)) { +export function canContinueWizardStep2( + draft: WorkOrderWizardDraft, + options?: WizardAccountGate, +): boolean { + const hasSite = Boolean(draft.locationId && (draft.siteCode.trim() || draft.locationName.trim())); + const hasAccount = !requiresAccount(options) || Boolean(draft.customer.trim()); + return Boolean(hasSite && hasAccount && draft.pocName.trim() && draft.pocPhone.trim()); +} + +export function wizardStep2ContinueHint( + draft: WorkOrderWizardDraft, + options?: WizardAccountGate, +): string | null { + if (canContinueWizardStep2(draft, options)) { return null; } if (!draft.locationId) { return "Pick a site to continue"; } - if (!draft.customer.trim()) { + if (requiresAccount(options) && !draft.customer.trim()) { return "Select an account to continue"; } return "Enter POC name and phone to continue"; } -export function canCreateWorkOrderFromWizard(draft: WorkOrderWizardDraft): boolean { - return canContinueWizardStep1(draft) && canContinueWizardStep2(draft); +export function canCreateWorkOrderFromWizard( + draft: WorkOrderWizardDraft, + options?: WizardAccountGate, +): boolean { + return canContinueWizardStep1(draft) && canContinueWizardStep2(draft, options); } -export function isWizardStepValid(step: number, draft: WorkOrderWizardDraft): boolean { +export function isWizardStepValid( + step: number, + draft: WorkOrderWizardDraft, + options?: WizardAccountGate, +): boolean { if (step === 1) { return canContinueWizardStep1(draft); } if (step === 2) { - return canContinueWizardStep2(draft); + return canContinueWizardStep2(draft, options); } return true; } diff --git a/src/domain/work-orders/utils/get-inline-create-missing.ts b/src/domain/work-orders/utils/get-inline-create-missing.ts index 5c2378f2..6c2ea6e2 100644 --- a/src/domain/work-orders/utils/get-inline-create-missing.ts +++ b/src/domain/work-orders/utils/get-inline-create-missing.ts @@ -18,9 +18,16 @@ export type InlineCreateDraftFields = { pocPhone?: string; }; -export function getInlineCreateMissing(d: InlineCreateDraftFields): InlineCreateReqField[] { +export type InlineCreateAccountGate = { + requireAccount?: boolean; +}; + +export function getInlineCreateMissing( + d: InlineCreateDraftFields, + options?: InlineCreateAccountGate, +): InlineCreateReqField[] { const miss: InlineCreateReqField[] = []; - if (!d.customer?.trim()) { + if (options?.requireAccount !== false && !d.customer?.trim()) { miss.push("customer"); } const hasSite = Boolean(d.site?.trim()) || Boolean(String(d.locationId ?? "").trim()); diff --git a/src/lib/auth/org-scope.ts b/src/lib/auth/org-scope.ts new file mode 100644 index 00000000..95afebd1 --- /dev/null +++ b/src/lib/auth/org-scope.ts @@ -0,0 +1,45 @@ +import type { AuthUser } from "@/domain/auth/types/auth-user"; + +const ORG_SCOPE_ALL = "all"; + +function readJwtClaims(token: string): Record | null { + const parts = token.split("."); + if (parts.length < 2) { + return null; + } + try { + const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/"); + const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="); + const parsed: unknown = JSON.parse(globalThis.atob(padded)); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + return null; + } + return parsed as Record; + } catch { + return null; + } +} + +function readStringClaim(source: Record | null, keys: string[]): string { + if (!source) { + return ""; + } + for (const key of keys) { + const value = source[key]; + if (typeof value === "string" && value.trim()) { + return value.trim(); + } + } + return ""; +} + +/** Org-wide only when the authenticated scope is explicitly `all`. Fail-closed otherwise. */ +export function isOrgWideScope(user: AuthUser | null | undefined): boolean { + if (!user) { + return false; + } + const claims = typeof user.token === "string" ? readJwtClaims(user.token) : null; + const fromJwt = readStringClaim(claims, ["org_scope", "orgScope", "OrgScope"]); + const fromUser = typeof user.orgScope === "string" ? user.orgScope.trim() : ""; + return (fromJwt || fromUser).toLowerCase() === ORG_SCOPE_ALL; +} diff --git a/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx b/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx index 4276fb3d..c6ccb517 100644 --- a/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx +++ b/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx @@ -1,6 +1,8 @@ import { fireEvent, screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { WizardStepLocationAccountSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-account-select"; +import type { AuthUser } from "@/domain/auth/types/auth-user"; +import { AuthContext, type AuthContextValue } from "@/providers/auth-context"; import { renderWithProviders } from "@/test/test-utils"; const { useAccountsListMock } = vi.hoisted(() => ({ @@ -12,9 +14,11 @@ vi.mock("@/hooks/use-debounce", () => ({ })); vi.mock("@/domain/accounts/use-cases/use-accounts-list", () => ({ - useAccountsList: (params: unknown) => useAccountsListMock(params), + useAccountsList: (params: unknown, enabled?: boolean) => useAccountsListMock(params, enabled), })); +const FOREIGN_ACCOUNT = "Contoso Other Tenant"; + const ACCOUNTS = { items: [ { @@ -28,8 +32,8 @@ const ACCOUNTS = { owner: "", }, { - id: 2, - name: "Sea Haven", + id: 7, + name: FOREIGN_ACCOUNT, number: "", industry: "", website: "", @@ -40,15 +44,60 @@ const ACCOUNTS = { ], }; +function encodeJwt(payload: Record): string { + const json = JSON.stringify(payload); + const b64 = btoa(json).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); + return `eyJhbGciOiJub25lIn0.${b64}.sig`; +} + +function authUser(token: string): AuthUser { + return { + token, + expiration: new Date(Date.now() + 60_000).toISOString(), + email: "user@seahavenind.com", + userRoles: "User", + phoneNumber: "", + fullname: "Test User", + id: 1, + }; +} + +function authValue(user: AuthUser): AuthContextValue { + return { + user, + isAuthenticated: true, + isLoading: false, + error: null, + login: vi.fn(), + logout: vi.fn(), + changePassword: vi.fn(), + isLoggingIn: false, + isLoggingOut: false, + isChangingPassword: false, + loginError: null, + }; +} + +function renderSelect(user: AuthUser) { + return renderWithProviders( + + + , + { withAuth: false }, + ); +} + describe("WizardStepLocationAccountSelect", () => { beforeEach(() => { useAccountsListMock.mockReturnValue({ data: ACCOUNTS }); }); - it("sets draft.customer to the exact account name on pick", () => { + it("sets draft.customer to the exact account name on pick for org-wide scope", () => { const onCustomerChange = vi.fn(); renderWithProviders( - , + + + , { withAuth: false }, ); @@ -56,21 +105,31 @@ describe("WizardStepLocationAccountSelect", () => { fireEvent.click(screen.getByRole("button", { name: "Amazon" })); expect(onCustomerChange).toHaveBeenCalledWith("Amazon"); + expect(useAccountsListMock).toHaveBeenCalledWith({ page: 1, pageSize: 50, search: "" }, true); }); - it("passes typed search to the accounts list query", () => { - renderWithProviders( - , - { withAuth: false }, - ); + it("passes typed search to the accounts list query for org-wide scope", () => { + renderSelect(authUser(encodeJwt({ org_scope: "all" }))); - expect(useAccountsListMock).toHaveBeenCalledWith({ page: 1, pageSize: 50, search: "" }); + expect(useAccountsListMock).toHaveBeenCalledWith({ page: 1, pageSize: 50, search: "" }, true); fireEvent.click(screen.getByRole("button", { name: /select account/i })); fireEvent.change(screen.getByPlaceholderText("Search account name…"), { target: { value: "Sea" }, }); - expect(useAccountsListMock).toHaveBeenCalledWith({ page: 1, pageSize: 50, search: "Sea" }); + expect(useAccountsListMock).toHaveBeenCalledWith( + { page: 1, pageSize: 50, search: "Sea" }, + true, + ); + }); + + it("does not fetch or expose another account name for a scoped caller", () => { + renderSelect(authUser(encodeJwt({ org_scope: "account", account_id: 7 }))); + + expect(useAccountsListMock).toHaveBeenCalledWith({ page: 1, pageSize: 50, search: "" }, false); + expect(screen.queryByRole("button", { name: /select account/i })).not.toBeInTheDocument(); + expect(screen.queryByText(FOREIGN_ACCOUNT)).not.toBeInTheDocument(); + expect(screen.queryByText("Amazon")).not.toBeInTheDocument(); }); }); diff --git a/src/test/app/(protected)/workorders/work-order-slide-over-failed-save.test.tsx b/src/test/app/(protected)/workorders/work-order-slide-over-failed-save.test.tsx index 9597efee..7e2ccec8 100644 --- a/src/test/app/(protected)/workorders/work-order-slide-over-failed-save.test.tsx +++ b/src/test/app/(protected)/workorders/work-order-slide-over-failed-save.test.tsx @@ -3,9 +3,13 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { WorkOrderSlideOver } from "@/app/(protected)/workorders/_components/detail/work-order-slide-over"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; -vi.mock("@/providers/auth-context", () => ({ - useAuthContext: () => ({ user: { id: "u1" } }), -})); +vi.mock("@/providers/auth-context", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useAuthContext: () => ({ user: { id: "u1" } }), + }; +}); vi.mock("@/domain/work-orders/use-cases/use-work-order-detail", () => ({ useWorkOrderBoardDetail: () => ({ diff --git a/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx b/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx index d62baec0..5b1e50c8 100644 --- a/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx +++ b/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx @@ -22,9 +22,13 @@ vi.mock("react-router", async () => { }; }); -vi.mock("@/providers/auth-context", () => ({ - useAuthContext: () => ({ user: { id: "u1" } }), -})); +vi.mock("@/providers/auth-context", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useAuthContext: () => ({ user: { id: "u1" } }), + }; +}); vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () => ({ useWorkOrdersListFilters: () => ({ diff --git a/src/test/app/(protected)/workorders/work-orders-board-core-affordances.test.tsx b/src/test/app/(protected)/workorders/work-orders-board-core-affordances.test.tsx index e8f8ce8e..2dbdc94e 100644 --- a/src/test/app/(protected)/workorders/work-orders-board-core-affordances.test.tsx +++ b/src/test/app/(protected)/workorders/work-orders-board-core-affordances.test.tsx @@ -16,9 +16,13 @@ vi.mock("react-router", async () => { }; }); -vi.mock("@/providers/auth-context", () => ({ - useAuthContext: () => ({ user: { id: "u1" } }), -})); +vi.mock("@/providers/auth-context", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useAuthContext: () => ({ user: { id: "u1" } }), + }; +}); vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () => ({ useWorkOrdersListFilters: () => ({ diff --git a/src/test/app/(protected)/workorders/work-orders-dispatcher-layout.test.tsx b/src/test/app/(protected)/workorders/work-orders-dispatcher-layout.test.tsx index 9458b1cd..246da779 100644 --- a/src/test/app/(protected)/workorders/work-orders-dispatcher-layout.test.tsx +++ b/src/test/app/(protected)/workorders/work-orders-dispatcher-layout.test.tsx @@ -12,9 +12,13 @@ vi.mock("react-router", async () => { }; }); -vi.mock("@/providers/auth-context", () => ({ - useAuthContext: () => ({ user: { id: "u1" } }), -})); +vi.mock("@/providers/auth-context", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useAuthContext: () => ({ user: { id: "u1" } }), + }; +}); vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () => ({ useWorkOrdersListFilters: () => ({ diff --git a/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts b/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts index 17c488f0..7079dcb0 100644 --- a/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts +++ b/src/test/domain/work-orders/mappers/wizard-draft-mapper.test.ts @@ -124,6 +124,21 @@ describe("wizard step validation", () => { expect(isWizardStepValid(2, draft)).toBe(false); }); + it("allows step 2 without customer when account is not required", () => { + const draft = { + ...EMPTY_WIZARD_DRAFT, + type: "PM" as const, + woNumber: "12345", + locationId: "99", + siteCode: "BK5", + pocName: "Jordan", + pocPhone: "555-0100", + }; + expect(canContinueWizardStep2(draft, { requireAccount: false })).toBe(true); + expect(isWizardStepValid(2, draft, { requireAccount: false })).toBe(true); + expect(wizardStep2ContinueHint(draft, { requireAccount: false })).toBeNull(); + }); + it("blocks step 2 without customer", () => { const draft = { ...EMPTY_WIZARD_DRAFT, diff --git a/src/test/domain/work-orders/utils/get-inline-create-missing.test.ts b/src/test/domain/work-orders/utils/get-inline-create-missing.test.ts index 6d2204df..250b4b1d 100644 --- a/src/test/domain/work-orders/utils/get-inline-create-missing.test.ts +++ b/src/test/domain/work-orders/utils/get-inline-create-missing.test.ts @@ -45,6 +45,21 @@ describe("getInlineCreateMissing", () => { ).toEqual(["pocName", "pocPhone"]); }); + it("skips account when the caller does not require it", () => { + expect( + getInlineCreateMissing( + { + site: "BK5", + locationId: "12", + woNumber: "25001", + pocName: "Jane", + pocPhone: "555-0100", + }, + { requireAccount: false }, + ), + ).toEqual([]); + }); + it("lists account, site, and woNumber when empty", () => { expect(getInlineCreateMissing({})).toEqual([ "customer", diff --git a/src/test/lib/auth/org-scope.test.ts b/src/test/lib/auth/org-scope.test.ts new file mode 100644 index 00000000..0782fd13 --- /dev/null +++ b/src/test/lib/auth/org-scope.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import type { AuthUser } from "@/domain/auth/types/auth-user"; +import { isOrgWideScope } from "@/lib/auth/org-scope"; + +function encodeJwt(payload: Record): string { + const json = JSON.stringify(payload); + const b64 = btoa(json).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); + return `eyJhbGciOiJub25lIn0.${b64}.sig`; +} + +function user(token: string, orgScope?: string): AuthUser { + return { + token, + expiration: new Date(Date.now() + 60_000).toISOString(), + email: "user@seahavenind.com", + userRoles: "User", + phoneNumber: "", + fullname: "Test User", + id: 1, + orgScope, + }; +} + +describe("isOrgWideScope", () => { + it("is false without a session", () => { + expect(isOrgWideScope(null)).toBe(false); + expect(isOrgWideScope(undefined)).toBe(false); + }); + + it("is true only when the JWT org_scope is all", () => { + expect(isOrgWideScope(user(encodeJwt({ org_scope: "all" })))).toBe(true); + expect(isOrgWideScope(user(encodeJwt({ org_scope: "ALL" })))).toBe(true); + }); + + it("is false for a claim-scoped account", () => { + expect(isOrgWideScope(user(encodeJwt({ org_scope: "account", account_id: 7 })))).toBe(false); + }); + + it("uses the login orgScope when the token is not a JWT", () => { + expect(isOrgWideScope(user("session-token", "all"))).toBe(true); + expect(isOrgWideScope(user("session-token", "account"))).toBe(false); + }); + + it("prefers a scoped JWT over a stale org-wide login field", () => { + expect(isOrgWideScope(user(encodeJwt({ org_scope: "account" }), "all"))).toBe(false); + }); + + it("is false when neither the JWT nor the user declares all", () => { + expect(isOrgWideScope(user("wo-visual-token"))).toBe(false); + }); +}); From 4f16b6a76f752639b1e7b2f9a5978d9f99af473e Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Tue, 25 Aug 2026 21:31:18 -0300 Subject: [PATCH 4/4] fix(work-orders): trust only JWT org_scope for account list Ignore stored AuthUser.orgScope so a scoped token cannot open the unscoped account picker via localStorage. --- src/domain/auth/types/auth-user.ts | 2 +- src/lib/auth/org-scope.ts | 11 +++++------ .../wizard-step-location-account-select.test.tsx | 11 ++++++++++- src/test/lib/auth/org-scope.test.ts | 13 ++++++++----- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/domain/auth/types/auth-user.ts b/src/domain/auth/types/auth-user.ts index f16f38b0..6a567b16 100644 --- a/src/domain/auth/types/auth-user.ts +++ b/src/domain/auth/types/auth-user.ts @@ -7,7 +7,7 @@ export interface AuthUser { fullname: string; id: string | number; name?: string; - /** JWT / login `org_scope`. Only `all` may see org-wide account lists. */ + /** Login payload field. Not trusted for org-wide UI gates; use the JWT claim. */ orgScope?: string; accountId?: string | number; } diff --git a/src/lib/auth/org-scope.ts b/src/lib/auth/org-scope.ts index 95afebd1..681a9e37 100644 --- a/src/lib/auth/org-scope.ts +++ b/src/lib/auth/org-scope.ts @@ -33,13 +33,12 @@ function readStringClaim(source: Record | null, keys: string[]) return ""; } -/** Org-wide only when the authenticated scope is explicitly `all`. Fail-closed otherwise. */ +/** Org-wide only when the JWT claim is explicitly `all`. Fail-closed otherwise. */ export function isOrgWideScope(user: AuthUser | null | undefined): boolean { - if (!user) { + if (!user || typeof user.token !== "string") { return false; } - const claims = typeof user.token === "string" ? readJwtClaims(user.token) : null; - const fromJwt = readStringClaim(claims, ["org_scope", "orgScope", "OrgScope"]); - const fromUser = typeof user.orgScope === "string" ? user.orgScope.trim() : ""; - return (fromJwt || fromUser).toLowerCase() === ORG_SCOPE_ALL; + const claims = readJwtClaims(user.token); + const scope = readStringClaim(claims, ["org_scope", "orgScope", "OrgScope"]); + return scope.toLowerCase() === ORG_SCOPE_ALL; } diff --git a/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx b/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx index c6ccb517..9acafb21 100644 --- a/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx +++ b/src/test/app/(protected)/workorders/wizard-step-location-account-select.test.tsx @@ -50,7 +50,7 @@ function encodeJwt(payload: Record): string { return `eyJhbGciOiJub25lIn0.${b64}.sig`; } -function authUser(token: string): AuthUser { +function authUser(token: string, orgScope?: string): AuthUser { return { token, expiration: new Date(Date.now() + 60_000).toISOString(), @@ -59,6 +59,7 @@ function authUser(token: string): AuthUser { phoneNumber: "", fullname: "Test User", id: 1, + orgScope, }; } @@ -132,4 +133,12 @@ describe("WizardStepLocationAccountSelect", () => { expect(screen.queryByText(FOREIGN_ACCOUNT)).not.toBeInTheDocument(); expect(screen.queryByText("Amazon")).not.toBeInTheDocument(); }); + + it("keeps the list disabled when a scoped token has no org_scope and storage says all", () => { + renderSelect(authUser(encodeJwt({ account_id: 7 }), "all")); + + expect(useAccountsListMock).toHaveBeenCalledWith({ page: 1, pageSize: 50, search: "" }, false); + expect(screen.queryByRole("button", { name: /select account/i })).not.toBeInTheDocument(); + expect(screen.queryByText(FOREIGN_ACCOUNT)).not.toBeInTheDocument(); + }); }); diff --git a/src/test/lib/auth/org-scope.test.ts b/src/test/lib/auth/org-scope.test.ts index 0782fd13..51f074c7 100644 --- a/src/test/lib/auth/org-scope.test.ts +++ b/src/test/lib/auth/org-scope.test.ts @@ -36,16 +36,19 @@ describe("isOrgWideScope", () => { expect(isOrgWideScope(user(encodeJwt({ org_scope: "account", account_id: 7 })))).toBe(false); }); - it("uses the login orgScope when the token is not a JWT", () => { - expect(isOrgWideScope(user("session-token", "all"))).toBe(true); - expect(isOrgWideScope(user("session-token", "account"))).toBe(false); + it("ignores a stored orgScope when the token is not a JWT", () => { + expect(isOrgWideScope(user("session-token", "all"))).toBe(false); }); - it("prefers a scoped JWT over a stale org-wide login field", () => { + it("ignores a stale stored orgScope when the JWT has no org_scope", () => { + expect(isOrgWideScope(user(encodeJwt({ account_id: 7 }), "all"))).toBe(false); + }); + + it("ignores a stale stored orgScope when the JWT is scoped", () => { expect(isOrgWideScope(user(encodeJwt({ org_scope: "account" }), "all"))).toBe(false); }); - it("is false when neither the JWT nor the user declares all", () => { + it("is false when the token has no org_scope claim", () => { expect(isOrgWideScope(user("wo-visual-token"))).toBe(false); }); });