Merge remote-tracking branch 'origin/dev' into fix/ab/sh-117-indicator-accessible-names

This commit is contained in:
Codex Review Integration 2026-08-26 15:32:37 -03:00
commit 57db887deb
30 changed files with 487 additions and 203 deletions

View file

@ -1,16 +1,18 @@
name: CI name: Frontend checks
on: on:
pull_request: pull_request:
branches: [main, dev] branches: [main, dev]
push: push:
branches: [main, dev] branches: [main, dev]
workflow_dispatch: {}
permissions: permissions:
contents: read contents: read
jobs: jobs:
ci: build-and-test:
name: Build and test
# Org reusable workflow (Node 24): format check, lint, build, unit tests. # Org reusable workflow (Node 24): format check, lint, build, unit tests.
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@main uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@main
with: with:
@ -28,9 +30,10 @@ jobs:
# GOVERNANCE_BASE points the changed-file gate at the right diff: # GOVERNANCE_BASE points the changed-file gate at the right diff:
# PR -> the PR target branch (origin/<base_ref>) # PR -> the PR target branch (origin/<base_ref>)
# push-> the previous commit on the branch (github.event.before) # push-> the previous commit on the branch (github.event.before)
# manual -> dev, for exact-head recovery runs
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
GOVERNANCE_BASE: ${{ github.event_name == 'pull_request' && format('origin/{0}', github.base_ref) || github.event.before }} GOVERNANCE_BASE: ${{ github.event_name == 'pull_request' && format('origin/{0}', github.base_ref) || github.event_name == 'push' && github.event.before || 'origin/dev' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -43,6 +46,7 @@ jobs:
- run: npm run verify - run: npm run verify
visual-regression: visual-regression:
name: Visual regression
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: mcr.microsoft.com/playwright:v1.61.1-noble container: mcr.microsoft.com/playwright:v1.61.1-noble
steps: steps:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 184 KiB

View file

@ -82,6 +82,10 @@ function isBoardDetail(pathname: string) {
return /\/workorders\/\d+\/detail$/.test(pathname); 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") { async function mockWorkOrderApi(page: Page, mode: "default" | "empty" | "error" = "default") {
await page.clock.setFixedTime(new Date(FROZEN_NOW)); await page.clock.setFixedTime(new Date(FROZEN_NOW));
await page.addInitScript((user) => localStorage.setItem("auth", JSON.stringify(user)), TEST_USER); 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; 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: [] }); await fulfillJson(route, { data: [] });
}); });
} }

View file

@ -195,7 +195,6 @@ function DrawerBody({
return ( return (
<Stack spacing={3} className="flex-1 overflow-y-auto" sx={{ p: 2.5 }}> <Stack spacing={3} className="flex-1 overflow-y-auto" sx={{ p: 2.5 }}>
<Box> <Box>
<SectionTitle>Company</SectionTitle>
<CompanySection roster={roster} /> <CompanySection roster={roster} />
</Box> </Box>
<Divider /> <Divider />

View file

@ -61,7 +61,9 @@ function VendorTableRow({ row, onOpenDetail, onOpenEdit }: VendorTableRowProps)
const trades = splitTrades(row.tradeSpecialties); const trades = splitTrades(row.tradeSpecialties);
const primaryTrade = trades[0] ?? "—"; const primaryTrade = trades[0] ?? "—";
const extraTrades = trades.length > 1 ? trades.length - 1 : 0; const extraTrades = trades.length > 1 ? trades.length - 1 : 0;
const locationText = [row.city, row.state].filter(Boolean).join(", ") || "—"; const cityStateText = [row.city.trim(), row.state.trim()].filter(Boolean).join(", ");
const locationText = cityStateText || row.address.trim() || "—";
const phoneText = row.phone.trim() || row.companyPhone.trim();
const rowKey = String(row.id ?? `company-${row.companyId ?? ""}`); const rowKey = String(row.id ?? `company-${row.companyId ?? ""}`);
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
@ -132,14 +134,14 @@ function VendorTableRow({ row, onOpenDetail, onOpenEdit }: VendorTableRowProps)
)} )}
</TableCell> </TableCell>
<TableCell sx={{ color: "var(--color-muted-foreground)" }}> <TableCell sx={{ color: "var(--color-muted-foreground)" }}>
{row.phone ? ( {phoneText ? (
<Link <Link
href={`tel:${row.phone}`} href={`tel:${phoneText}`}
underline="hover" underline="hover"
color="inherit" color="inherit"
onClick={stopPropagation} onClick={stopPropagation}
> >
{row.phone} {phoneText}
</Link> </Link>
) : ( ) : (
"—" "—"

View file

@ -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);
},
};
}

View file

@ -1,10 +1,9 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { buildSiteDialogPatch } from "@/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch"; 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 type { SitePatch } from "@/app/(protected)/workorders/_components/list/table/cells/site-dialog-types";
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 { useLocationDetail } from "@/domain/locations/use-cases/use-location-detail";
import { formatLocationAddressPreview } from "@/domain/locations/mappers/location-mapper"; 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 { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact";
import type { LocationOption } from "@/domain/work-orders/types/work-order"; import type { LocationOption } from "@/domain/work-orders/types/work-order";
@ -37,55 +36,28 @@ export function useSiteDialogState({
createMode, createMode,
onSave, onSave,
}: UseSiteDialogStateArgs) { }: UseSiteDialogStateArgs) {
const [code, setCode] = useState(value); const fields = useSiteDialogFormFields({
const [locId, setLocId] = useState(() => resolveLocationId(locationId, value, sites)); open,
const [pn, setPn] = useState(pocName); value,
const [pp, setPp] = useState(pocPhone); locationId,
const [notes, setNotes] = useState(pocNotes); pocName,
const [extraContacts, setExtraContacts] = useState(additionalContacts); pocPhone,
const [pocFilledFor, setPocFilledFor] = useState(""); pocNotes,
const [dirty, setDirty] = useState(false); additionalContacts,
const [contactsDirty, setContactsDirty] = useState(false); sites,
const [baselineHadContacts, setBaselineHadContacts] = useState( createMode,
() => additionalContacts.length > 0, });
); const { locId, pocFilledFor, pn, pp, setPn, setPp, setPocFilledFor, siteMissing } = fields;
const [showErrors, setShowErrors] = useState(false);
const { const {
data: locationDetail, data: locationDetail,
isLoading: locationDetailLoading, isLoading: locationDetailLoading,
isError: locationDetailError, isError: locationDetailError,
} = useLocationDetail(open && locId ? locId : undefined); } = useLocationDetail(open && locId ? locId : undefined);
const selected = sites.find((s) => String(s.id) === locId);
const addressPreview = locationDetail ? formatLocationAddressPreview(locationDetail) : ""; const addressPreview = locationDetail ? formatLocationAddressPreview(locationDetail) : "";
const siteMissing = !locId;
const pocMissing = !pn.trim() || !pp.trim(); const pocMissing = !pn.trim() || !pp.trim();
const canConfirm = !siteMissing && (!createMode || !pocMissing); const canConfirm = !siteMissing && (!createMode || !pocMissing);
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]);
useEffect(() => { useEffect(() => {
if (!open || !locId || !locationDetail || pocFilledFor === locId) { if (!open || !locId || !locationDetail || pocFilledFor === locId) {
return; return;
@ -93,82 +65,49 @@ export function useSiteDialogState({
setPn((prev) => (prev.trim() ? prev : (locationDetail.contact ?? ""))); setPn((prev) => (prev.trim() ? prev : (locationDetail.contact ?? "")));
setPp((prev) => (prev.trim() ? prev : (locationDetail.phone ?? ""))); setPp((prev) => (prev.trim() ? prev : (locationDetail.phone ?? "")));
setPocFilledFor(locId); setPocFilledFor(locId);
}, [open, locId, locationDetail, pocFilledFor]); }, [open, locId, locationDetail, pocFilledFor, setPn, setPp, setPocFilledFor]);
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,
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);
};
const attemptSave = () => { const attemptSave = () => {
if (createMode && !canConfirm) { if (createMode && !canConfirm) {
setShowErrors(true); fields.setShowErrors(true);
return; return;
} }
onSave(buildPatch()); onSave(
buildSiteDialogPatch({
code: fields.code,
locId: fields.locId,
selected: fields.selected,
pocName: fields.pn,
pocPhone: fields.pp,
pocNotes: fields.notes,
extraContacts: fields.extraContacts,
contactsDirty: fields.contactsDirty,
baselineHadContacts: fields.baselineHadContacts,
}),
);
onOpenChange(false); onOpenChange(false);
}; };
return { return {
locId, locId: fields.locId,
code, code: fields.code,
selected, selected: fields.selected,
locationDetail, locationDetail,
locationDetailLoading, locationDetailLoading,
locationDetailError, locationDetailError,
addressPreview, addressPreview,
pn, pn: fields.pn,
pp, pp: fields.pp,
notes, notes: fields.notes,
extraContacts, extraContacts: fields.extraContacts,
showErrors, showErrors: fields.showErrors,
siteMissing, siteMissing: fields.siteMissing,
handlePick, handlePick: fields.handlePick,
attemptSave, attemptSave,
setPn: (next: string) => markPocDirty(() => setPn(next)), setPn: fields.setPnDirty,
setPp: (next: string) => markPocDirty(() => setPp(next)), setPp: fields.setPpDirty,
setNotes: (next: string) => markPocDirty(() => setNotes(next)), setNotes: fields.setNotesDirty,
setExtraContacts: markContactsDirty, setExtraContacts: fields.setExtraContactsDirty,
close: () => onOpenChange(false), close: () => onOpenChange(false),
}; };
} }

View file

@ -21,7 +21,6 @@ import {
inlineCreateSaveHint, inlineCreateSaveHint,
inlineCreateSiteError, inlineCreateSiteError,
} from "@/domain/work-orders/utils/get-inline-create-missing"; } from "@/domain/work-orders/utils/get-inline-create-missing";
import { WO_TABLE_ROW_HEIGHT } from "./wo-table-cols"; import { WO_TABLE_ROW_HEIGHT } from "./wo-table-cols";
import { WoTableInlineRowCells } from "./wo-table-inline-row-cells"; import { WoTableInlineRowCells } from "./wo-table-inline-row-cells";
import { WoTableInlineRowActions } from "./wo-table-inline-row-actions"; import { WoTableInlineRowActions } from "./wo-table-inline-row-actions";

View file

@ -17,6 +17,7 @@ type WizardFieldSelectProps = {
search?: boolean; search?: boolean;
searchPlaceholder?: string; searchPlaceholder?: string;
disabled?: boolean; disabled?: boolean;
onSearchChange?: (query: string) => void;
}; };
function filterOptions(options: WizardMenuOption[], query: string): WizardMenuOption[] { function filterOptions(options: WizardMenuOption[], query: string): WizardMenuOption[] {
@ -41,15 +42,22 @@ export function WizardFieldSelect({
search = true, search = true,
searchPlaceholder = "Search…", searchPlaceholder = "Search…",
disabled = false, disabled = false,
onSearchChange,
}: WizardFieldSelectProps) { }: WizardFieldSelectProps) {
const { anchorEl, open, setAnchor, close } = useMenuAnchor(); const { anchorEl, open, setAnchor, close } = useMenuAnchor();
const [q, setQ] = useState(""); const [q, setQ] = useState("");
const selected = options.find((o) => o.key === value); 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 = () => { const handleClose = () => {
close(); close();
setQ(""); setQ("");
onSearchChange?.("");
}; };
return ( return (
@ -117,7 +125,7 @@ export function WizardFieldSelect({
search={search} search={search}
searchPlaceholder={searchPlaceholder} searchPlaceholder={searchPlaceholder}
query={q} query={q}
onQueryChange={setQ} onQueryChange={handleQueryChange}
options={filtered} options={filtered}
value={value} value={value}
onPick={(key) => { onPick={(key) => {

View file

@ -2,11 +2,10 @@ import { Check, ChevronLeft, ChevronRight, Lightbulb } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
WIZARD_STEPS, WIZARD_STEPS,
canContinueWizardStep1,
canContinueWizardStep2,
canCreateWorkOrderFromWizard, canCreateWorkOrderFromWizard,
isWizardStepValid, isWizardStepValid,
isWizardWoNumberValid, wizardStep1ContinueHint,
wizardStep2ContinueHint,
type WorkOrderWizardDraft, type WorkOrderWizardDraft,
} from "@/domain/work-orders/types/work-order-wizard"; } from "@/domain/work-orders/types/work-order-wizard";
@ -22,15 +21,11 @@ type WizardFooterProps = {
}; };
function stepHint(step: number, draft: WorkOrderWizardDraft): string | null { function stepHint(step: number, draft: WorkOrderWizardDraft): string | null {
if (step === 1 && !canContinueWizardStep1(draft)) { if (step === 1) {
if (!isWizardWoNumberValid(draft.woNumber)) { return wizardStep1ContinueHint(draft);
return "Enter an alphanumeric WO number, or leave blank for SH generation";
}
return "Select severity to continue";
} }
if (step === 2 && !canContinueWizardStep2(draft)) { if (step === 2) {
if (!draft.locationId) return "Pick a site to continue"; return wizardStep2ContinueHint(draft);
return "Enter POC name and phone to continue";
} }
return null; return null;
} }

View file

@ -1,4 +1,3 @@
import { Text } from "@/components/ui/text";
import type { LocationOption } from "@/domain/work-orders/types/work-order"; import type { LocationOption } from "@/domain/work-orders/types/work-order";
import type { Location } from "@/domain/locations/types/location"; import type { Location } from "@/domain/locations/types/location";
import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard"; import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard";
@ -73,16 +72,6 @@ export function WizardStepLocationService({
onUpdate={onUpdate} onUpdate={onUpdate}
onPatch={onPatch} onPatch={onPatch}
/> />
{!draft.locationId && !draft.pm && (
<Text
as="p"
variant="description"
sx={{ fontSize: 12, fontStyle: "italic", color: "var(--muted-foreground)" }}
>
Both are optional — leave blank to fill in later from the dispatch board.
</Text>
)}
</div> </div>
); );
} }

View file

@ -4,6 +4,7 @@ import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order
import { import {
WO_TYPE_META, WO_TYPE_META,
WO_TYPES, WO_TYPES,
WIZARD_WO_NUMBER_MAX_LENGTH,
isWizardWoNumberValid, isWizardWoNumberValid,
type WOType, type WOType,
} from "@/domain/work-orders/types/work-order-wizard"; } from "@/domain/work-orders/types/work-order-wizard";
@ -231,28 +232,6 @@ function clearSchedulePatch(): Partial<WorkOrderWizardDraft> {
}; };
} }
function WizardAddOnOverrideField({
checked,
onChange,
}: {
checked: boolean;
onChange: (value: boolean) => void;
}) {
return (
<label className="flex cursor-pointer items-center gap-2">
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
aria-label="Mark as Add-On"
/>
<Text as="p" variant="description" sx={{ fontSize: 13, margin: 0 }}>
Mark as Add-On
</Text>
</label>
);
}
export function WizardStepTypeSchedule({ export function WizardStepTypeSchedule({
draft, draft,
users, users,
@ -294,19 +273,25 @@ export function WizardStepTypeSchedule({
type="text" type="text"
value={draft.woNumber} value={draft.woNumber}
onChange={(e) => onChange={(e) =>
onUpdate("woNumber", e.target.value.replace(/[^a-zA-Z0-9]/g, "").slice(0, 20)) onUpdate(
"woNumber",
e.target.value.replace(/[^a-zA-Z0-9]/g, "").slice(0, WIZARD_WO_NUMBER_MAX_LENGTH),
)
} }
onBlur={() => onDuplicateWoNumber?.(draft.woNumber)} onBlur={() => onDuplicateWoNumber?.(draft.woNumber)}
placeholder="Leave blank for SH provisional number" placeholder="Leave blank for SH provisional number"
maxLength={20} maxLength={WIZARD_WO_NUMBER_MAX_LENGTH}
className={DLG_INPUT_CLS} className={DLG_INPUT_CLS}
style={DLG_INPUT_STYLE} style={DLG_INPUT_STYLE}
/> />
{Boolean(woNumberError) && ( <Text
<Text as="p" variant="error" sx={{ fontSize: 11, marginTop: "6px" }}> as="p"
WO number must be alphanumeric. variant="error"
</Text> when={Boolean(woNumberError)}
)} sx={{ fontSize: 11, marginTop: "6px" }}
>
WO number must be alphanumeric.
</Text>
</div> </div>
<div> <div>
@ -338,13 +323,6 @@ export function WizardStepTypeSchedule({
</div> </div>
<WizardTypeScheduleDates draft={draft} onUpdate={onUpdate} onPatch={onPatch} /> <WizardTypeScheduleDates draft={draft} onUpdate={onUpdate} onPatch={onPatch} />
{Boolean(!hasWizardSchedule(draft)) && (
<WizardAddOnOverrideField
checked={Boolean(draft.isAddOnOverride)}
onChange={(value) => onUpdate("isAddOnOverride", value)}
/>
)}
</div> </div>
); );
} }

View file

@ -7,6 +7,9 @@ export interface AuthUser {
fullname: string; fullname: string;
id: string | number; id: string | number;
name?: string; name?: string;
/** Login payload field. Not trusted for org-wide UI gates; use the JWT claim. */
orgScope?: string;
accountId?: string | number;
} }
export interface LoginCredentials { export interface LoginCredentials {

View file

@ -119,6 +119,7 @@ export function mapVendorListItem(raw: unknown): VendorListItem {
googleMapsUrl: readString(item, "googleMapsUrl", "GoogleMapsUrl"), googleMapsUrl: readString(item, "googleMapsUrl", "GoogleMapsUrl"),
notes: readString(item, "notes", "Notes"), notes: readString(item, "notes", "Notes"),
totalJobs: readNumber(item, "totalJobs", "TotalJobs"), totalJobs: readNumber(item, "totalJobs", "TotalJobs"),
address: readString(item, "address", "Address"),
city: readString(item, "city", "City"), city: readString(item, "city", "City"),
state: readString(item, "state", "State"), state: readString(item, "state", "State"),
tradeSpecialties: readString(item, "tradeSpecialties", "TradeSpecialties"), tradeSpecialties: readString(item, "tradeSpecialties", "TradeSpecialties"),

View file

@ -13,6 +13,7 @@ export interface VendorListItem {
googleMapsUrl: string; googleMapsUrl: string;
notes: string; notes: string;
totalJobs: number; totalJobs: number;
address: string;
city: string; city: string;
state: string; state: string;
tradeSpecialties: string; tradeSpecialties: string;

View file

@ -103,13 +103,15 @@ export const EMPTY_WIZARD_DRAFT: WorkOrderWizardDraft = {
isAddOnOverride: false, isAddOnOverride: false,
}; };
/** Blank numbers receive an SH provisional number from the backend; nonblank values are alphanumeric. */ export const WIZARD_WO_NUMBER_MAX_LENGTH = 20;
/** Blank is valid (backend generates an SH provisional number). Otherwise alphanumeric, max 20. */
export function isWizardWoNumberValid(woNumber: string): boolean { export function isWizardWoNumberValid(woNumber: string): boolean {
const trimmed = woNumber.trim(); const trimmed = woNumber.trim();
if (!trimmed) { if (!trimmed) {
return true; return true;
} }
return trimmed.length <= 20 && /^[a-z0-9]+$/i.test(trimmed); return trimmed.length <= WIZARD_WO_NUMBER_MAX_LENGTH && /^[a-z0-9]+$/i.test(trimmed);
} }
/** Type is optional at creation; empty draft type defaults to PM. */ /** Type is optional at creation; empty draft type defaults to PM. */
@ -194,11 +196,31 @@ export function canContinueWizardStep1(draft: WorkOrderWizardDraft): boolean {
return true; return true;
} }
export function wizardStep1ContinueHint(draft: WorkOrderWizardDraft): string | null {
if (canContinueWizardStep1(draft)) {
return null;
}
if (!isWizardWoNumberValid(draft.woNumber)) {
return "WO number must be alphanumeric";
}
return "Select severity to continue";
}
export function canContinueWizardStep2(draft: WorkOrderWizardDraft): boolean { export function canContinueWizardStep2(draft: WorkOrderWizardDraft): boolean {
const hasSite = Boolean(draft.locationId && (draft.siteCode.trim() || draft.locationName.trim())); const hasSite = Boolean(draft.locationId && (draft.siteCode.trim() || draft.locationName.trim()));
return Boolean(hasSite && draft.pocName.trim() && draft.pocPhone.trim()); return Boolean(hasSite && draft.pocName.trim() && draft.pocPhone.trim());
} }
export function wizardStep2ContinueHint(draft: WorkOrderWizardDraft): string | null {
if (canContinueWizardStep2(draft)) {
return null;
}
if (!draft.locationId) {
return "Pick a site to continue";
}
return "Enter POC name and phone to continue";
}
export function canCreateWorkOrderFromWizard(draft: WorkOrderWizardDraft): boolean { export function canCreateWorkOrderFromWizard(draft: WorkOrderWizardDraft): boolean {
return canContinueWizardStep1(draft) && canContinueWizardStep2(draft); return canContinueWizardStep1(draft) && canContinueWizardStep2(draft);
} }

View file

@ -33,6 +33,7 @@ const vendor: VendorListItem = {
googleMapsUrl: "", googleMapsUrl: "",
notes: "", notes: "",
totalJobs: 0, totalJobs: 0,
address: "",
city: "", city: "",
state: "", state: "",
tradeSpecialties: "", tradeSpecialties: "",
@ -312,6 +313,36 @@ describe("VendorDetailDrawer design parity", () => {
expect(screen.getAllByText("Gateway Plumbing").length).toBeGreaterThan(0); expect(screen.getAllByText("Gateway Plumbing").length).toBeGreaterThan(0);
}); });
it("shows exactly one Company label above the company value in view mode", () => {
useVendorCompanyRoster.mockReturnValue(
rosterWith([{ id: 1, contactName: "Adam", phone: "", isActive: true, totalJobs: 5 }]),
);
renderWithProviders(<VendorDetailDrawer open={true} vendor={vendor} onClose={vi.fn()} />, {
route: "/vendors",
withAuth: false,
});
expect(screen.getAllByText("Company")).toHaveLength(1);
expect(screen.getAllByText("Gateway Plumbing").length).toBeGreaterThan(0);
});
it("keeps the Notes section heading when notes exist", () => {
const roster = rosterWith([
{ id: 1, contactName: "Adam", phone: "", isActive: true, totalJobs: 5 },
]);
roster.data.notes = "Preferred after-hours vendor";
useVendorCompanyRoster.mockReturnValue(roster);
renderWithProviders(<VendorDetailDrawer open={true} vendor={vendor} onClose={vi.fn()} />, {
route: "/vendors",
withAuth: false,
});
expect(screen.getByText("Notes")).toBeInTheDocument();
expect(screen.getByText("Preferred after-hours vendor")).toBeInTheDocument();
});
it("labels the Google Maps field below Address even when no URL is stored", () => { it("labels the Google Maps field below Address even when no URL is stored", () => {
useVendorCompanyRoster.mockReturnValue( useVendorCompanyRoster.mockReturnValue(
rosterWith([{ id: 1, contactName: "Adam", phone: "", isActive: true, totalJobs: 5 }]), rosterWith([{ id: 1, contactName: "Adam", phone: "", isActive: true, totalJobs: 5 }]),

View file

@ -40,6 +40,7 @@ const activeVendor: VendorListItem = {
googleMapsUrl: "", googleMapsUrl: "",
notes: "", notes: "",
totalJobs: 0, totalJobs: 0,
address: "1 Market St",
city: "St. Louis", city: "St. Louis",
state: "MO", state: "MO",
tradeSpecialties: "Backflow Preventers", tradeSpecialties: "Backflow Preventers",
@ -133,6 +134,53 @@ describe("VendorsListPage", () => {
expect(screen.queryByRole("button", { name: "Deactivate vendor" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Deactivate vendor" })).not.toBeInTheDocument();
}); });
it("prefers trimmed city and state over address and technician phone over company phone", () => {
setupDefaults();
const vendorWithAllFields: VendorListItem = {
...activeVendor,
id: 3,
address: " 1 Market St ",
city: " St. Louis ",
state: " MO ",
phone: " (314) 555-0198 ",
};
useVendorsList.mockImplementation((params: { isActive?: boolean; pageSize?: number }) => {
if (params.pageSize === 1) return result([], 1);
return params.isActive ? result([vendorWithAllFields], 1) : result([], 0);
});
renderWithProviders(<VendorsListPage />, { route: "/vendors", withAuth: false });
expect(screen.getByText("St. Louis, MO")).toBeInTheDocument();
expect(screen.queryByText(/1 Market St/)).not.toBeInTheDocument();
const phoneLink = screen.getByRole("link", { name: "(314) 555-0198" });
expect(phoneLink).toHaveAttribute("href", "tel:(314) 555-0198");
expect(screen.queryByText("(314) 555-0100")).not.toBeInTheDocument();
});
it("falls back to the trimmed address and company phone when technician fields are absent", () => {
setupDefaults();
const fallbackVendor: VendorListItem = {
...activeVendor,
id: 3,
address: " 1 Market St ",
city: " ",
state: "",
phone: " ",
};
useVendorsList.mockImplementation((params: { isActive?: boolean; pageSize?: number }) => {
if (params.pageSize === 1) return result([], 1);
return params.isActive ? result([fallbackVendor], 1) : result([], 0);
});
renderWithProviders(<VendorsListPage />, { route: "/vendors", withAuth: false });
expect(screen.getByText("1 Market St")).toBeInTheDocument();
const companyPhoneLink = screen.getByRole("link", { name: "(314) 555-0100" });
expect(companyPhoneLink).toHaveAttribute("href", "tel:(314) 555-0100");
expect(screen.queryByText(/555-0198/)).not.toBeInTheDocument();
});
it("reports the filtered totalCount in the footer when page rows are fewer than total", () => { it("reports the filtered totalCount in the footer when page rows are fewer than total", () => {
setupDefaults(); setupDefaults();
const pageVendors = Array.from({ length: 3 }, (_, index) => ({ const pageVendors = Array.from({ length: 3 }, (_, index) => ({

View file

@ -30,6 +30,8 @@ describe("buildInlineDraftCreateInput", () => {
expect(payload.scheduleWeekOnly).toBeUndefined(); expect(payload.scheduleWeekOnly).toBeUndefined();
expect(payload.woNumber).toBe("25001"); expect(payload.woNumber).toBe("25001");
expect(payload.pocName).toBe("Jane"); expect(payload.pocName).toBe("Jane");
expect(payload.locationId).toBe(9);
expect(payload).not.toHaveProperty("customer");
}); });
it("keeps week-only schedule when targetWeek is set", () => { it("keeps week-only schedule when targetWeek is set", () => {

View file

@ -0,0 +1,11 @@
import { describe, expect, it } from "vitest";
import { buildWizardSidebarSummaryRows } from "@/app/(protected)/workorders/_components/wizard/wizard-sidebar-summary-rows";
import { EMPTY_WIZARD_DRAFT } from "@/domain/work-orders/types/work-order-wizard";
describe("buildWizardSidebarSummaryRows", () => {
it("omits Account from the live summary", () => {
const labels = buildWizardSidebarSummaryRows(EMPTY_WIZARD_DRAFT).map(([label]) => label);
expect(labels).not.toContain("Account");
expect(labels).toContain("Site");
});
});

View file

@ -0,0 +1,28 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { WizardStepLocationService } from "@/app/(protected)/workorders/_components/wizard/wizard-step-location-service";
import { EMPTY_WIZARD_DRAFT } from "@/domain/work-orders/types/work-order-wizard";
describe("WizardStepLocationService", () => {
it("requires Site, keeps Service optional, and does not render Account", () => {
render(
<WizardStepLocationService
draft={EMPTY_WIZARD_DRAFT}
locations={[]}
locationDetail={undefined}
services={[]}
svcOpen={false}
onSvcOpenChange={vi.fn()}
svcSearch=""
onSvcSearchChange={vi.fn()}
onUpdate={vi.fn()}
onPatch={vi.fn()}
/>,
);
expect(screen.getByText("Site")).toBeInTheDocument();
expect(screen.getByText("Service")).toBeInTheDocument();
expect(screen.queryByText("Account")).not.toBeInTheDocument();
expect(screen.queryByText(/Both are optional/i)).not.toBeInTheDocument();
});
});

View file

@ -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 { WorkOrderSlideOver } from "@/app/(protected)/workorders/_components/detail/work-order-slide-over";
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
vi.mock("@/providers/auth-context", () => ({ vi.mock("@/providers/auth-context", async (importOriginal) => {
useAuthContext: () => ({ user: { id: "u1" } }), const actual = await importOriginal<typeof import("@/providers/auth-context")>();
})); return {
...actual,
useAuthContext: () => ({ user: { id: "u1" } }),
};
});
vi.mock("@/domain/work-orders/use-cases/use-work-order-detail", () => ({ vi.mock("@/domain/work-orders/use-cases/use-work-order-detail", () => ({
useWorkOrderBoardDetail: () => ({ useWorkOrderBoardDetail: () => ({

View file

@ -22,9 +22,13 @@ vi.mock("react-router", async () => {
}; };
}); });
vi.mock("@/providers/auth-context", () => ({ vi.mock("@/providers/auth-context", async (importOriginal) => {
useAuthContext: () => ({ user: { id: "u1" } }), const actual = await importOriginal<typeof import("@/providers/auth-context")>();
})); return {
...actual,
useAuthContext: () => ({ user: { id: "u1" } }),
};
});
vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () => ({ vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () => ({
useWorkOrdersListFilters: () => ({ useWorkOrdersListFilters: () => ({

View file

@ -16,9 +16,13 @@ vi.mock("react-router", async () => {
}; };
}); });
vi.mock("@/providers/auth-context", () => ({ vi.mock("@/providers/auth-context", async (importOriginal) => {
useAuthContext: () => ({ user: { id: "u1" } }), const actual = await importOriginal<typeof import("@/providers/auth-context")>();
})); return {
...actual,
useAuthContext: () => ({ user: { id: "u1" } }),
};
});
vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () => ({ vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () => ({
useWorkOrdersListFilters: () => ({ useWorkOrdersListFilters: () => ({
@ -226,9 +230,7 @@ describe("WorkOrdersListPage board-core public interface", () => {
expect( expect(
screen.getByPlaceholderText(/leave blank for sh provisional number/i), screen.getByPlaceholderText(/leave blank for sh provisional number/i),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByRole("button", { name: /continue/i })).toBeEnabled();
fireEvent.click(screen.getByRole("button", { name: /continue/i })); fireEvent.click(screen.getByRole("button", { name: /continue/i }));
expect(screen.queryByText(/wo number is required/i)).not.toBeInTheDocument();
expect(screen.getByRole("heading", { name: /location & service/i })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: /location & service/i })).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: /^back$/i })); fireEvent.click(screen.getByRole("button", { name: /^back$/i }));

View file

@ -12,9 +12,13 @@ vi.mock("react-router", async () => {
}; };
}); });
vi.mock("@/providers/auth-context", () => ({ vi.mock("@/providers/auth-context", async (importOriginal) => {
useAuthContext: () => ({ user: { id: "u1" } }), const actual = await importOriginal<typeof import("@/providers/auth-context")>();
})); return {
...actual,
useAuthContext: () => ({ user: { id: "u1" } }),
};
});
vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () => ({ vi.mock("@/app/(protected)/workorders/_hooks/use-work-orders-list-filters", () => ({
useWorkOrdersListFilters: () => ({ useWorkOrdersListFilters: () => ({

View file

@ -28,6 +28,7 @@ describe("vendorsApi.getList", () => {
CompanyId: "co-1", CompanyId: "co-1",
CompanyName: "Gateway Plumbing", CompanyName: "Gateway Plumbing",
ContactName: "Adam Whyte", ContactName: "Adam Whyte",
Address: "1 Market St",
City: "St. Louis", City: "St. Louis",
State: "MO", State: "MO",
IsActive: false, IsActive: false,
@ -45,6 +46,7 @@ describe("vendorsApi.getList", () => {
searchParams: { search: "gateway", isActive: "false", page: 1, pageSize: 12 }, searchParams: { search: "gateway", isActive: "false", page: 1, pageSize: 12 },
}); });
expect(result.items[0]).toMatchObject({ expect(result.items[0]).toMatchObject({
address: "1 Market St",
city: "St. Louis", city: "St. Louis",
state: "MO", state: "MO",
isActive: false, isActive: false,

View file

@ -114,6 +114,7 @@ describe("workOrdersApi.createFromBoard", () => {
type: "PM", type: "PM",
siteCode: "BK5", siteCode: "BK5",
woNumber: "12345", woNumber: "12345",
locationId: "9",
}); });
expect(apiPost).toHaveBeenCalledWith(API_PATHS.workOrder.boardCreate, { expect(apiPost).toHaveBeenCalledWith(API_PATHS.workOrder.boardCreate, {
@ -122,6 +123,7 @@ describe("workOrdersApi.createFromBoard", () => {
lifecycleStatus: 1, lifecycleStatus: 1,
woNumber: "12345", woNumber: "12345",
isAddOn: false, isAddOn: false,
locationId: 9,
}); });
}); });
}); });

View file

@ -7,6 +7,8 @@ import {
canCreateWorkOrderFromWizard, canCreateWorkOrderFromWizard,
isWizardStepValid, isWizardStepValid,
isWizardWoNumberValid, isWizardWoNumberValid,
wizardStep1ContinueHint,
wizardStep2ContinueHint,
} from "@/domain/work-orders/types/work-order-wizard"; } from "@/domain/work-orders/types/work-order-wizard";
describe("mapWizardDraftToFormValues", () => { describe("mapWizardDraftToFormValues", () => {
@ -65,18 +67,18 @@ describe("mapWizardDraftToFormValues", () => {
}); });
describe("wizard step validation", () => { describe("wizard step validation", () => {
it("allows step 1 continue without a woNumber for backend SH generation", () => { it("allows step 1 with a blank woNumber for backend SH generation", () => {
expect(canContinueWizardStep1(EMPTY_WIZARD_DRAFT)).toBe(true); expect(canContinueWizardStep1(EMPTY_WIZARD_DRAFT)).toBe(true);
expect(isWizardStepValid(1, EMPTY_WIZARD_DRAFT)).toBe(true); expect(isWizardStepValid(1, EMPTY_WIZARD_DRAFT)).toBe(true);
}); });
it("allows step 1 without type when woNumber is set (type defaults to PM)", () => { it("allows step 1 without type when woNumber is set", () => {
const draft = { ...EMPTY_WIZARD_DRAFT, woNumber: "12345" }; const draft = { ...EMPTY_WIZARD_DRAFT, woNumber: "12345" };
expect(canContinueWizardStep1(draft)).toBe(true); expect(canContinueWizardStep1(draft)).toBe(true);
expect(isWizardStepValid(1, draft)).toBe(true); expect(isWizardStepValid(1, draft)).toBe(true);
}); });
it("allows step 1 when woNumber is missing even with type", () => { it("allows step 1 without woNumber when type is set", () => {
const draft = { ...EMPTY_WIZARD_DRAFT, type: "PM" as const }; const draft = { ...EMPTY_WIZARD_DRAFT, type: "PM" as const };
expect(canContinueWizardStep1(draft)).toBe(true); expect(canContinueWizardStep1(draft)).toBe(true);
expect(isWizardStepValid(1, draft)).toBe(true); expect(isWizardStepValid(1, draft)).toBe(true);
@ -123,6 +125,35 @@ describe("wizard step validation", () => {
expect(isWizardStepValid(2, draft)).toBe(false); expect(isWizardStepValid(2, draft)).toBe(false);
}); });
it("allows step 2 without an account when site and POC are set", () => {
const draft = {
...EMPTY_WIZARD_DRAFT,
type: "PM" as const,
woNumber: "12345",
locationId: "99",
siteCode: "BK5",
pocName: "Jordan",
pocPhone: "555-0100",
};
expect(canContinueWizardStep2(draft)).toBe(true);
expect(isWizardStepValid(2, draft)).toBe(true);
expect(wizardStep2ContinueHint(draft)).toBeNull();
});
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 but no phone", () => {
const draft = {
...EMPTY_WIZARD_DRAFT,
locationId: "99",
siteCode: "BK5",
pocName: "Jordan",
};
expect(wizardStep2ContinueHint(draft)).toBe("Enter POC name and phone to continue");
});
it("allows step 2 when site and POC are set", () => { it("allows step 2 when site and POC are set", () => {
const draft = { const draft = {
...EMPTY_WIZARD_DRAFT, ...EMPTY_WIZARD_DRAFT,
@ -135,6 +166,7 @@ describe("wizard step validation", () => {
}; };
expect(canContinueWizardStep2(draft)).toBe(true); expect(canContinueWizardStep2(draft)).toBe(true);
expect(isWizardStepValid(2, draft)).toBe(true); expect(isWizardStepValid(2, draft)).toBe(true);
expect(wizardStep2ContinueHint(draft)).toBeNull();
}); });
it("blocks create without siteCode", () => { it("blocks create without siteCode", () => {
@ -152,7 +184,7 @@ describe("wizard step validation", () => {
const draft = { const draft = {
...EMPTY_WIZARD_DRAFT, ...EMPTY_WIZARD_DRAFT,
type: "PM" as const, type: "PM" as const,
woNumber: "12345678901234567890", woNumber: "12345678901",
locationId: "99", locationId: "99",
siteCode: "BK5", siteCode: "BK5",
pocName: "Jordan", pocName: "Jordan",
@ -163,18 +195,10 @@ describe("wizard step validation", () => {
}); });
it("allows provisional SH-prefixed woNumber", () => { it("allows provisional SH-prefixed woNumber", () => {
const draft = {
...EMPTY_WIZARD_DRAFT,
woNumber: "SH12345",
};
expect(canContinueWizardStep1(draft)).toBe(true);
});
it("accepts blank woNumber for backend SH generation", () => {
const draft = { const draft = {
...EMPTY_WIZARD_DRAFT, ...EMPTY_WIZARD_DRAFT,
type: "PM" as const, type: "PM" as const,
woNumber: "", woNumber: "SH12345",
}; };
expect(isWizardWoNumberValid(draft.woNumber)).toBe(true); expect(isWizardWoNumberValid(draft.woNumber)).toBe(true);
expect(canContinueWizardStep1(draft)).toBe(true); expect(canContinueWizardStep1(draft)).toBe(true);
@ -192,10 +216,22 @@ describe("wizard step validation", () => {
it("rejects non-alphanumeric woNumber", () => { it("rejects non-alphanumeric woNumber", () => {
expect(isWizardWoNumberValid("!!!")).toBe(false); expect(isWizardWoNumberValid("!!!")).toBe(false);
expect(canContinueWizardStep1({ ...EMPTY_WIZARD_DRAFT, woNumber: "!!!" })).toBe(false); expect(canContinueWizardStep1({ ...EMPTY_WIZARD_DRAFT, type: "PM", woNumber: "!!!" })).toBe(
false,
);
}); });
it("accepts alphanumeric woNumber", () => { it("accepts alphanumeric woNumber", () => {
expect(isWizardWoNumberValid("ABC123")).toBe(true); expect(isWizardWoNumberValid("ABC123")).toBe(true);
}); });
it("hints alphanumeric WO number or severity when step 1 is incomplete", () => {
expect(wizardStep1ContinueHint(EMPTY_WIZARD_DRAFT)).toBeNull();
expect(wizardStep1ContinueHint({ ...EMPTY_WIZARD_DRAFT, type: "PM", woNumber: "!!!" })).toBe(
"WO number must be alphanumeric",
);
expect(wizardStep1ContinueHint({ ...EMPTY_WIZARD_DRAFT, type: "Emergency" })).toBe(
"Select severity to continue",
);
});
}); });

View file

@ -60,6 +60,29 @@ describe("mapWizardDraftToBoardCreatePayload", () => {
}); });
}); });
it("sends locationId and never customer on create", () => {
const payload = mapWizardDraftToBoardCreatePayload({
...EMPTY_WIZARD_DRAFT,
type: "PM",
siteCode: "BK5",
locationId: "12",
});
expect(payload.locationId).toBe(12);
expect(payload).not.toHaveProperty("customer");
});
it("allows create mapping without a service", () => {
const payload = mapWizardDraftToBoardCreatePayload({
...EMPTY_WIZARD_DRAFT,
type: "PM",
siteCode: "BK5",
locationId: "12",
});
expect(payload.primaryService).toBeUndefined();
});
it("sets isAddOn from cutoff preview when scheduled (create hint; not frozen)", () => { it("sets isAddOn from cutoff preview when scheduled (create hint; not frozen)", () => {
const payload = mapWizardDraftToBoardCreatePayload({ const payload = mapWizardDraftToBoardCreatePayload({
...EMPTY_WIZARD_DRAFT, ...EMPTY_WIZARD_DRAFT,