mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 19:22:06 +00:00
Merge branch 'dev' into fix/ab/sh-341-generate-completes-work-order
This commit is contained in:
commit
33a979c79d
10 changed files with 216 additions and 33 deletions
|
|
@ -22,6 +22,8 @@ type EditableWONumberCellProps = {
|
||||||
onSave: (v: string) => void;
|
onSave: (v: string) => void;
|
||||||
onDuplicateFound?: (existing: WorkOrderTableRow) => void;
|
onDuplicateFound?: (existing: WorkOrderTableRow) => void;
|
||||||
locked?: boolean;
|
locked?: boolean;
|
||||||
|
/** Empty-state label for create flows; renders with a persistent required asterisk. */
|
||||||
|
requiredPlaceholder?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function resolveDuplicateWoNumber(
|
async function resolveDuplicateWoNumber(
|
||||||
|
|
@ -34,6 +36,28 @@ async function resolveDuplicateWoNumber(
|
||||||
return remote && String(remote.id) !== String(currentId) ? remote : undefined;
|
return remote && String(remote.id) !== String(currentId) ? remote : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EmptyWoNumberLabel({
|
||||||
|
value,
|
||||||
|
q,
|
||||||
|
requiredPlaceholder,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
q?: string;
|
||||||
|
requiredPlaceholder?: string;
|
||||||
|
}) {
|
||||||
|
if (value) {
|
||||||
|
return <HighlightText text={value} q={q} />;
|
||||||
|
}
|
||||||
|
if (!requiredPlaceholder) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span className="font-sans text-[13px] font-normal">
|
||||||
|
{requiredPlaceholder} <span style={{ color: "var(--destructive)" }}>*</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function EditableWONumberCell({
|
export function EditableWONumberCell({
|
||||||
value,
|
value,
|
||||||
rescheduleCount,
|
rescheduleCount,
|
||||||
|
|
@ -49,6 +73,7 @@ export function EditableWONumberCell({
|
||||||
onSave,
|
onSave,
|
||||||
onDuplicateFound,
|
onDuplicateFound,
|
||||||
locked = false,
|
locked = false,
|
||||||
|
requiredPlaceholder,
|
||||||
}: EditableWONumberCellProps) {
|
}: EditableWONumberCellProps) {
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [draft, setDraft] = useState(value);
|
const [draft, setDraft] = useState(value);
|
||||||
|
|
@ -162,7 +187,7 @@ export function EditableWONumberCell({
|
||||||
padding: "2px 4px",
|
padding: "2px 4px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{value ? <HighlightText text={value} q={q} /> : "-"}
|
<EmptyWoNumberLabel value={value} q={q} requiredPlaceholder={requiredPlaceholder} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<AddOnPill when={isAddOn} />
|
<AddOnPill when={isAddOn} />
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ type SiteDialogPocSectionProps = {
|
||||||
pocNotes: string;
|
pocNotes: string;
|
||||||
additionalContacts: WorkOrderAdditionalContact[];
|
additionalContacts: WorkOrderAdditionalContact[];
|
||||||
showPrimaryErrors?: boolean;
|
showPrimaryErrors?: boolean;
|
||||||
|
primaryRequired?: boolean;
|
||||||
onPocNameChange: (value: string) => void;
|
onPocNameChange: (value: string) => void;
|
||||||
onPocPhoneChange: (value: string) => void;
|
onPocPhoneChange: (value: string) => void;
|
||||||
onPocNotesChange: (value: string) => void;
|
onPocNotesChange: (value: string) => void;
|
||||||
|
|
@ -19,6 +20,7 @@ export function SiteDialogPocSection({
|
||||||
pocNotes,
|
pocNotes,
|
||||||
additionalContacts,
|
additionalContacts,
|
||||||
showPrimaryErrors = false,
|
showPrimaryErrors = false,
|
||||||
|
primaryRequired = false,
|
||||||
onPocNameChange,
|
onPocNameChange,
|
||||||
onPocPhoneChange,
|
onPocPhoneChange,
|
||||||
onPocNotesChange,
|
onPocNotesChange,
|
||||||
|
|
@ -31,6 +33,7 @@ export function SiteDialogPocSection({
|
||||||
pocNotes={pocNotes}
|
pocNotes={pocNotes}
|
||||||
additionalContacts={additionalContacts}
|
additionalContacts={additionalContacts}
|
||||||
showPrimaryErrors={showPrimaryErrors}
|
showPrimaryErrors={showPrimaryErrors}
|
||||||
|
primaryRequired={primaryRequired}
|
||||||
onPocNameChange={onPocNameChange}
|
onPocNameChange={onPocNameChange}
|
||||||
onPocPhoneChange={onPocPhoneChange}
|
onPocPhoneChange={onPocPhoneChange}
|
||||||
onPocNotesChange={onPocNotesChange}
|
onPocNotesChange={onPocNotesChange}
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ export function SiteDialog({
|
||||||
|
|
||||||
<fieldset disabled={viewOnly} className="mt-3 space-y-3 border-0 p-0">
|
<fieldset disabled={viewOnly} className="mt-3 space-y-3 border-0 p-0">
|
||||||
<div>
|
<div>
|
||||||
<FieldLabel>Site</FieldLabel>
|
<FieldLabel required={createMode}>Site</FieldLabel>
|
||||||
<WizardFieldSelect
|
<WizardFieldSelect
|
||||||
value={s.locId}
|
value={s.locId}
|
||||||
placeholder="Select site …"
|
placeholder="Select site …"
|
||||||
|
|
@ -130,6 +130,7 @@ export function SiteDialog({
|
||||||
pocNotes={s.notes}
|
pocNotes={s.notes}
|
||||||
additionalContacts={s.extraContacts}
|
additionalContacts={s.extraContacts}
|
||||||
showPrimaryErrors={createMode && s.showErrors}
|
showPrimaryErrors={createMode && s.showErrors}
|
||||||
|
primaryRequired={createMode}
|
||||||
onPocNameChange={s.setPn}
|
onPocNameChange={s.setPn}
|
||||||
onPocPhoneChange={s.setPp}
|
onPocPhoneChange={s.setPp}
|
||||||
onPocNotesChange={s.setNotes}
|
onPocNotesChange={s.setNotes}
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ export function WoTableInlineIdentityCells({
|
||||||
allOrders={allRows as never[]}
|
allOrders={allRows as never[]}
|
||||||
onSave={(v) => onPatch({ woNumber: v })}
|
onSave={(v) => onPatch({ woNumber: v })}
|
||||||
onDuplicateFound={onDuplicateFound}
|
onDuplicateFound={onDuplicateFound}
|
||||||
|
requiredPlaceholder="Insert WO #"
|
||||||
/>
|
/>
|
||||||
<AddOnOverrideToggle
|
<AddOnOverrideToggle
|
||||||
checked={draft.isAddOnOverride}
|
checked={draft.isAddOnOverride}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export function WoTableInlineRowActions({
|
||||||
onCancel,
|
onCancel,
|
||||||
}: WoTableInlineRowActionsProps) {
|
}: WoTableInlineRowActionsProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-0.5">
|
<div className="flex items-center gap-0.5" style={{ minHeight: 34 }}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { RefObject } from "react";
|
import type { ReactNode, RefObject } from "react";
|
||||||
import type {
|
import type {
|
||||||
LocationOption,
|
LocationOption,
|
||||||
VendorDropdownItem,
|
VendorDropdownItem,
|
||||||
|
|
@ -21,6 +21,7 @@ import { StatusCell } from "./cells/status-cell";
|
||||||
import { TypeCell } from "./cells/type-cell";
|
import { TypeCell } from "./cells/type-cell";
|
||||||
import { VendorCell } from "./cells/vendor-cell";
|
import { VendorCell } from "./cells/vendor-cell";
|
||||||
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
|
import { toVendorTablePatch } from "@/domain/work-orders/utils/vendor-assignment-patch";
|
||||||
|
import { InlineCreateField } from "./inline-create-field";
|
||||||
import { toInlineDraftRow } from "./to-inline-draft-row";
|
import { toInlineDraftRow } from "./to-inline-draft-row";
|
||||||
import { WoTableInlineIdentityCells } from "./wo-table-inline-identity-cells";
|
import { WoTableInlineIdentityCells } from "./wo-table-inline-identity-cells";
|
||||||
|
|
||||||
|
|
@ -43,6 +44,17 @@ type WoTableInlineRowCellsProps = {
|
||||||
onRequestComplete?: () => void;
|
onRequestComplete?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Matches the identity cells: top-aligned so field boxes and error helpers line up. */
|
||||||
|
const INLINE_FIELD_TD_STYLE = { padding: "8px 6px 0", verticalAlign: "top" } as const;
|
||||||
|
|
||||||
|
function InlineFieldCell({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<td className="group/cell" style={INLINE_FIELD_TD_STYLE}>
|
||||||
|
<InlineCreateField>{children}</InlineCreateField>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function InlineTypeSeverityCell({
|
function InlineTypeSeverityCell({
|
||||||
draft,
|
draft,
|
||||||
onPatch,
|
onPatch,
|
||||||
|
|
@ -51,20 +63,22 @@ function InlineTypeSeverityCell({
|
||||||
onPatch: (patch: Partial<WorkOrderTableDraft>) => void;
|
onPatch: (patch: Partial<WorkOrderTableDraft>) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<td className="group/cell" style={{ padding: "0 14px" }}>
|
<InlineFieldCell>
|
||||||
<TypeCell
|
<TypeCell
|
||||||
value={draft.type}
|
value={draft.type}
|
||||||
severity={draft.severity}
|
severity={draft.severity}
|
||||||
onSave={(t, sev) => onPatch({ type: t, severity: sev })}
|
onSave={(t, sev) => onPatch({ type: t, severity: sev })}
|
||||||
/>
|
/>
|
||||||
</td>
|
</InlineFieldCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InlineMutedDashCell() {
|
function InlineMutedDashCell() {
|
||||||
return (
|
return (
|
||||||
<td style={{ padding: "0 14px" }}>
|
<td style={INLINE_FIELD_TD_STYLE}>
|
||||||
<span style={{ fontSize: 11, color: "var(--color-text-muted)" }}>—</span>
|
<div className="flex items-center" style={{ minHeight: 34 }}>
|
||||||
|
<span style={{ fontSize: 11, color: "var(--color-text-muted)" }}>—</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -81,13 +95,12 @@ function InlineScheduleOnCell({
|
||||||
const hasSchedule =
|
const hasSchedule =
|
||||||
Boolean(draft.scheduledOn) || Boolean(draft.targetWeek) || Boolean(draft.scheduleEnd);
|
Boolean(draft.scheduledOn) || Boolean(draft.targetWeek) || Boolean(draft.scheduleEnd);
|
||||||
return (
|
return (
|
||||||
<td className="group/cell" style={{ padding: "0 14px" }}>
|
<InlineFieldCell>
|
||||||
<DateCell
|
<DateCell
|
||||||
value={draft.scheduledOn}
|
value={draft.scheduledOn}
|
||||||
allowWeekMode
|
allowWeekMode
|
||||||
targetWeek={draft.targetWeek}
|
targetWeek={draft.targetWeek}
|
||||||
defaultMode={weekDefault ? "week" : "date"}
|
defaultMode={weekDefault ? "week" : "date"}
|
||||||
highlight={!draft.scheduledOn && !draft.targetWeek}
|
|
||||||
scheduleEnd={draft.scheduleEnd}
|
scheduleEnd={draft.scheduleEnd}
|
||||||
schedMode={draft.schedMode}
|
schedMode={draft.schedMode}
|
||||||
onSave={(date) =>
|
onSave={(date) =>
|
||||||
|
|
@ -142,7 +155,7 @@ function InlineScheduleOnCell({
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</td>
|
</InlineFieldCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,26 +197,25 @@ export function WoTableInlineRowCells({
|
||||||
|
|
||||||
<InlineTypeSeverityCell draft={draft} onPatch={onPatch} />
|
<InlineTypeSeverityCell draft={draft} onPatch={onPatch} />
|
||||||
|
|
||||||
<td className="group/cell" style={{ padding: "0 14px" }}>
|
<InlineFieldCell>
|
||||||
<AssignedToCell
|
<AssignedToCell
|
||||||
dispatcherId={draft.dispatcherId}
|
dispatcherId={draft.dispatcherId}
|
||||||
users={users}
|
users={users}
|
||||||
highlight={!draft.dispatcherId}
|
|
||||||
onSave={(id) => {
|
onSave={(id) => {
|
||||||
onPatch({ dispatcherId: id, status: draft.status || "Incomplete" });
|
onPatch({ dispatcherId: id, status: draft.status || "Incomplete" });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</InlineFieldCell>
|
||||||
<InlineScheduleOnCell draft={draft} weekDefault={weekDefault} onPatch={onPatch} />
|
<InlineScheduleOnCell draft={draft} weekDefault={weekDefault} onPatch={onPatch} />
|
||||||
|
|
||||||
<td className="group/cell" style={{ padding: "0 14px" }}>
|
<InlineFieldCell>
|
||||||
<DueDateCell
|
<DueDateCell
|
||||||
type={draft.type}
|
type={draft.type}
|
||||||
value={draft.dueDate}
|
value={draft.dueDate}
|
||||||
onSave={(date) => onPatch({ dueDate: date })}
|
onSave={(date) => onPatch({ dueDate: date })}
|
||||||
/>
|
/>
|
||||||
</td>
|
</InlineFieldCell>
|
||||||
<td className="group/cell" style={{ padding: "0 14px" }}>
|
<InlineFieldCell>
|
||||||
<PMTypeCell
|
<PMTypeCell
|
||||||
value={draft.pm}
|
value={draft.pm}
|
||||||
pmTypes={pmTypes}
|
pmTypes={pmTypes}
|
||||||
|
|
@ -221,8 +233,8 @@ export function WoTableInlineRowCells({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</td>
|
</InlineFieldCell>
|
||||||
<td className="group/cell" style={{ padding: "0 14px" }}>
|
<InlineFieldCell>
|
||||||
<VendorCell
|
<VendorCell
|
||||||
vendorId={draft.vendorId}
|
vendorId={draft.vendorId}
|
||||||
company={draft.company}
|
company={draft.company}
|
||||||
|
|
@ -231,8 +243,8 @@ export function WoTableInlineRowCells({
|
||||||
vendors={vendors}
|
vendors={vendors}
|
||||||
onSave={(p) => onPatch(toVendorTablePatch(p))}
|
onSave={(p) => onPatch(toVendorTablePatch(p))}
|
||||||
/>
|
/>
|
||||||
</td>
|
</InlineFieldCell>
|
||||||
<td className="group/cell" style={{ padding: "0 14px" }}>
|
<InlineFieldCell>
|
||||||
<ApptTimeCell
|
<ApptTimeCell
|
||||||
apptTime={
|
apptTime={
|
||||||
draft.apptStart ? `${draft.apptStart}${draft.apptEnd ? ` – ${draft.apptEnd}` : ""}` : ""
|
draft.apptStart ? `${draft.apptStart}${draft.apptEnd ? ` – ${draft.apptEnd}` : ""}` : ""
|
||||||
|
|
@ -242,8 +254,8 @@ export function WoTableInlineRowCells({
|
||||||
onPatch({ apptStart: start ?? "", apptEnd: end ?? "" });
|
onPatch({ apptStart: start ?? "", apptEnd: end ?? "" });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</InlineFieldCell>
|
||||||
<td className="group/cell" style={{ padding: "0 14px" }}>
|
<InlineFieldCell>
|
||||||
<StatusCell
|
<StatusCell
|
||||||
wo={draftRow as never}
|
wo={draftRow as never}
|
||||||
statusOptions={CREATION_INLINE_STATUSES}
|
statusOptions={CREATION_INLINE_STATUSES}
|
||||||
|
|
@ -251,7 +263,7 @@ export function WoTableInlineRowCells({
|
||||||
onOpenSlideOver={() => {}}
|
onOpenSlideOver={() => {}}
|
||||||
onRequestComplete={onRequestComplete}
|
onRequestComplete={onRequestComplete}
|
||||||
/>
|
/>
|
||||||
</td>
|
</InlineFieldCell>
|
||||||
|
|
||||||
<InlineMutedDashCell />
|
<InlineMutedDashCell />
|
||||||
<InlineMutedDashCell />
|
<InlineMutedDashCell />
|
||||||
|
|
|
||||||
|
|
@ -115,15 +115,17 @@ export function WoTableInlineRow({
|
||||||
pointerEvents: saving ? "none" : undefined,
|
pointerEvents: saving ? "none" : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<td style={{ padding: "0 0 0 6px" }} />
|
<td style={{ padding: "8px 0 0 6px", verticalAlign: "top" }} />
|
||||||
|
|
||||||
<td style={{ padding: "0 2px 0 8px" }}>
|
<td style={{ padding: "8px 2px 0 8px", verticalAlign: "top" }}>
|
||||||
<Flag
|
<div className="flex items-center" style={{ minHeight: 34 }}>
|
||||||
size={14}
|
<Flag
|
||||||
strokeWidth={1.75}
|
size={14}
|
||||||
aria-hidden="true"
|
strokeWidth={1.75}
|
||||||
style={{ color: "var(--color-border)" }}
|
aria-hidden="true"
|
||||||
/>
|
style={{ color: "var(--color-border)" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<WoTableInlineRowCells
|
<WoTableInlineRowCells
|
||||||
|
|
@ -145,7 +147,7 @@ export function WoTableInlineRow({
|
||||||
onRequestComplete={() => setConfirmComplete(true)}
|
onRequestComplete={() => setConfirmComplete(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<td style={{ padding: "0 4px" }}>
|
<td style={{ padding: "8px 4px 0", verticalAlign: "top" }}>
|
||||||
<WoTableInlineRowActions
|
<WoTableInlineRowActions
|
||||||
canSave={canSave}
|
canSave={canSave}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useId } from "react";
|
||||||
import { Plus, Trash2 } from "lucide-react";
|
import { Plus, Trash2 } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Text } from "@/components/ui/text";
|
import { Text } from "@/components/ui/text";
|
||||||
|
|
@ -23,6 +24,8 @@ type PocContactFieldsProps = {
|
||||||
onAdditionalContactsChange: (contacts: WorkOrderAdditionalContact[]) => void;
|
onAdditionalContactsChange: (contacts: WorkOrderAdditionalContact[]) => void;
|
||||||
helperText?: string;
|
helperText?: string;
|
||||||
showPrimaryErrors?: boolean;
|
showPrimaryErrors?: boolean;
|
||||||
|
/** Labels the primary name/phone inputs with a persistent required asterisk. */
|
||||||
|
primaryRequired?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function extraNameMissing(contact: WorkOrderAdditionalContact): boolean {
|
function extraNameMissing(contact: WorkOrderAdditionalContact): boolean {
|
||||||
|
|
@ -135,7 +138,9 @@ export function PocContactFields({
|
||||||
onAdditionalContactsChange,
|
onAdditionalContactsChange,
|
||||||
helperText,
|
helperText,
|
||||||
showPrimaryErrors = false,
|
showPrimaryErrors = false,
|
||||||
|
primaryRequired = false,
|
||||||
}: PocContactFieldsProps) {
|
}: PocContactFieldsProps) {
|
||||||
|
const fieldId = useId();
|
||||||
const nameMissing = showPrimaryErrors && !pocName.trim();
|
const nameMissing = showPrimaryErrors && !pocName.trim();
|
||||||
const phoneMissing = showPrimaryErrors && !pocPhone.trim();
|
const phoneMissing = showPrimaryErrors && !pocPhone.trim();
|
||||||
|
|
||||||
|
|
@ -153,7 +158,13 @@ export function PocContactFields({
|
||||||
)}
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>
|
<div>
|
||||||
|
{primaryRequired && (
|
||||||
|
<FieldLabel required htmlFor={`${fieldId}-name`}>
|
||||||
|
POC name
|
||||||
|
</FieldLabel>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
|
id={`${fieldId}-name`}
|
||||||
value={pocName}
|
value={pocName}
|
||||||
onChange={(event) => onPocNameChange(event.target.value)}
|
onChange={(event) => onPocNameChange(event.target.value)}
|
||||||
placeholder="POC name"
|
placeholder="POC name"
|
||||||
|
|
@ -168,7 +179,13 @@ export function PocContactFields({
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
{primaryRequired && (
|
||||||
|
<FieldLabel required htmlFor={`${fieldId}-phone`}>
|
||||||
|
POC phone
|
||||||
|
</FieldLabel>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
|
id={`${fieldId}-phone`}
|
||||||
value={pocPhone}
|
value={pocPhone}
|
||||||
onChange={(event) => onPocPhoneChange(event.target.value)}
|
onChange={(event) => onPocPhoneChange(event.target.value)}
|
||||||
placeholder="POC phone"
|
placeholder="POC phone"
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export function WizardStepLocationPocFields({
|
||||||
onAdditionalContactsChange={(contacts) => onUpdate("additionalContacts", contacts)}
|
onAdditionalContactsChange={(contacts) => onUpdate("additionalContacts", contacts)}
|
||||||
helperText="Auto-filled from records — edit if outdated"
|
helperText="Auto-filled from records — edit if outdated"
|
||||||
showPrimaryErrors={showErrors && pocIncomplete}
|
showPrimaryErrors={showErrors && pocIncomplete}
|
||||||
|
primaryRequired
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { fireEvent, screen } from "@testing-library/react";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { PocContactFields } from "@/app/(protected)/workorders/_components/poc/poc-contact-fields";
|
||||||
|
import { WoTableInlineRow } from "@/app/(protected)/workorders/_components/list/table/wo-table-inline-row";
|
||||||
|
import {
|
||||||
|
EMPTY_TABLE_DRAFT,
|
||||||
|
type WorkOrderTableDraft,
|
||||||
|
} from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
import { renderWithProviders } from "@/test/test-utils";
|
||||||
|
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/type-cell", () => ({
|
||||||
|
TypeCell: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/assigned-to-cell", () => ({
|
||||||
|
AssignedToCell: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/date-cell", () => ({
|
||||||
|
DateCell: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/due-date-cell", () => ({
|
||||||
|
DueDateCell: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/pm-type-cell", () => ({
|
||||||
|
PMTypeCell: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/vendor-cell", () => ({
|
||||||
|
VendorCell: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/appt-time-cell", () => ({
|
||||||
|
ApptTimeCell: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(protected)/workorders/_components/list/table/cells/status-cell", () => ({
|
||||||
|
StatusCell: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function renderInlineRow(draft: WorkOrderTableDraft, onSave = vi.fn()) {
|
||||||
|
const ui = (d: WorkOrderTableDraft) => (
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<WoTableInlineRow
|
||||||
|
draft={d}
|
||||||
|
draftDayKey="2026-06-01"
|
||||||
|
users={[]}
|
||||||
|
sites={[]}
|
||||||
|
vendors={[]}
|
||||||
|
pmTypes={[]}
|
||||||
|
allRows={[]}
|
||||||
|
onChange={vi.fn()}
|
||||||
|
onSave={onSave}
|
||||||
|
onCancel={vi.fn()}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
const view = renderWithProviders(ui(draft), { withAuth: false });
|
||||||
|
return { ...view, rerenderDraft: (d: WorkOrderTableDraft) => view.rerender(ui(d)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMPLETE_DRAFT: WorkOrderTableDraft = {
|
||||||
|
...EMPTY_TABLE_DRAFT,
|
||||||
|
site: "BK5",
|
||||||
|
locationId: 9,
|
||||||
|
woNumber: "12345678901",
|
||||||
|
pocName: "Dana",
|
||||||
|
pocPhone: "555-0100",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("WoTableInlineRow required fields", () => {
|
||||||
|
it("marks Site and WO # as required without showing errors before a Save attempt", () => {
|
||||||
|
renderInlineRow(EMPTY_TABLE_DRAFT);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: /^Site \*$/ })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Insert WO #")).toHaveTextContent("Insert WO # *");
|
||||||
|
expect(screen.queryByText("Site is required")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Work order number is required")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reveals field errors only after Save is attempted, then clears them once corrected", () => {
|
||||||
|
const onSave = vi.fn();
|
||||||
|
const { rerenderDraft } = renderInlineRow(EMPTY_TABLE_DRAFT, onSave);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Save work order" }));
|
||||||
|
|
||||||
|
expect(onSave).not.toHaveBeenCalled();
|
||||||
|
expect(screen.getByText("Site is required")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Work order number is required")).toBeInTheDocument();
|
||||||
|
|
||||||
|
rerenderDraft(COMPLETE_DRAFT);
|
||||||
|
|
||||||
|
expect(screen.queryByText("Site is required")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Work order number is required")).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Save work order" }));
|
||||||
|
expect(onSave).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("PocContactFields required primary contact", () => {
|
||||||
|
it("labels POC name and phone with a required asterisk when the contact is required", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<PocContactFields
|
||||||
|
pocName=""
|
||||||
|
pocPhone=""
|
||||||
|
pocNotes=""
|
||||||
|
additionalContacts={[]}
|
||||||
|
primaryRequired
|
||||||
|
onPocNameChange={vi.fn()}
|
||||||
|
onPocPhoneChange={vi.fn()}
|
||||||
|
onPocNotesChange={vi.fn()}
|
||||||
|
onAdditionalContactsChange={vi.fn()}
|
||||||
|
/>,
|
||||||
|
{ withAuth: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText(/POC name/)).toHaveAttribute("placeholder", "POC name");
|
||||||
|
expect(screen.getByLabelText(/POC phone/)).toHaveAttribute("placeholder", "POC phone");
|
||||||
|
expect(screen.getByText("POC name").closest("label")).toHaveTextContent("POC name*");
|
||||||
|
expect(screen.getByText("POC phone").closest("label")).toHaveTextContent("POC phone*");
|
||||||
|
expect(screen.queryByText("Contact name is required")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue