diff --git a/docs/work-orders/pending-be-contract.md b/docs/work-orders/pending-be-contract.md new file mode 100644 index 00000000..08aff844 --- /dev/null +++ b/docs/work-orders/pending-be-contract.md @@ -0,0 +1,30 @@ +# Work-order `additionalContacts` contract (SH-218) + +Authoritative frontend contract for additional POC 2+ contacts. Implemented on +backend PR [shoc-backend#66](https://github.com/Sea-Haven-Industries/shoc-backend/pull/66). + +## Shape + +```ts +{ name: string; phone: string; notes?: string } +``` + +- Primary POC remains `pocName` / `pocPhone` / `pocNotes`. +- `additionalContacts` holds ordered POC 2+ only. +- Notes are optional. An empty list (or omitted field) is valid. +- Retained entries require **both** `name` and `phone`. Fully blank placeholder + rows are dropped and must not fail create. Name-only or phone-only entries are + rejected on POST create and PATCH. + +## Endpoints + +| Operation | Where | +| ------------------ | ------------------------------------------------------------- | +| POST create | `additionalContacts` array on the board create body | +| GET board / search | `additionalContacts` on each board row | +| GET detail | `info.additionalContacts` (same ordered list as board) | +| PATCH | `field: "additionalContacts"`, `value` is a JSON array string | + +Until #66 is merged to the deployed API, a missing field is still mapped as `[]` +on the frontend so the UI does not throw. After merge, create → reload → +board/slide-over must round-trip the ordered list through GET detail `info`. diff --git a/e2e/work-orders/week-day-navigation.spec.ts b/e2e/work-orders/week-day-navigation.spec.ts new file mode 100644 index 00000000..5ec5402b --- /dev/null +++ b/e2e/work-orders/week-day-navigation.spec.ts @@ -0,0 +1,124 @@ +import { expect, test, type Page, type Route } from "@playwright/test"; + +const FROZEN_NOW = "2026-08-19T15:00:00.000Z"; + +const TEST_USER = { + token: "wo-week-day-e2e-token", + expiration: "2030-01-01T00:00:00.000Z", + email: "admin@seahavenind.com", + userRoles: "Admin", + phoneNumber: "5551234567", + fullname: "Work Order E2E Admin", + id: "1", +}; + +async function fulfillJson(route: Route, body: unknown, status = 200) { + await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body) }); +} + +function boardRow(id: number, woNumber: string, scheduledDate: string | null) { + return { + id, + woNumber, + workOrderType: 2, + siteCode: woNumber, + lifecycleStatusLabel: "Scheduled", + scheduledDate, + dispatcherId: "1", + dispatcherName: "Pat Dispatcher", + pm: "HVAC", + }; +} + +async function mockWorkOrderBoard(page: Page) { + await page.route("**/api/**", (route) => { + const pathname = new URL(route.request().url()).pathname; + if (!pathname.startsWith("/api/")) { + return route.fallback(); + } + if (pathname.includes("/workorders/board")) { + return route.fallback(); + } + return fulfillJson(route, { data: [] }); + }); + await page.route("**/api/workorders/board*", (route) => { + if (route.request().url().includes("board/search")) { + return route.fallback(); + } + const url = new URL(route.request().url()); + const weekStart = url.searchParams.get("weekStart") ?? "2026-08-17"; + const scheduled = + weekStart === "2026-08-17" + ? [boardRow(1, "MON-WO", "2026-08-17"), boardRow(2, "WED-WO", "2026-08-19")] + : [boardRow(4, "PREV-MON-WO", "2026-08-10")]; + return fulfillJson(route, { + weekStart, + weekEnd: weekStart === "2026-08-17" ? "2026-08-21" : "2026-08-14", + counts: { returned: scheduled.length + 1, total: scheduled.length + 1 }, + unscheduled: [boardRow(3, "UNSCHED-WO", null)], + scheduled, + }); + }); + await page.route("**/api/workorders/board/search*", (route) => + fulfillJson(route, { items: [], totalCount: 0, page: 0, pageSize: 50 }), + ); +} + +test.describe("Work Orders week/day navigation (SH-240)", () => { + test.beforeEach(async ({ page }) => { + await page.clock.install({ time: new Date(FROZEN_NOW) }); + await page.clock.resume(); + await page.addInitScript( + (user) => localStorage.setItem("auth", JSON.stringify(user)), + TEST_USER, + ); + await mockWorkOrderBoard(page); + }); + + test("scopes the table to one day and excludes Unscheduled", async ({ page }) => { + await page.goto("/workorders"); + await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({ + timeout: 15_000, + }); + await expect(page.getByRole("button", { name: "Week 8/17–8/21" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Week 8/17–8/21" })).toContainText("Live"); + + await expect(page.getByRole("tab", { name: "Mon · 08/17" })).toBeVisible(); + await expect(page.getByRole("tab", { name: "Wed · 08/19" })).toHaveAttribute( + "aria-selected", + "true", + ); + + await expect(page.getByText("WED-WO").first()).toBeVisible(); + await expect(page.getByText("MON-WO")).toHaveCount(0); + await expect(page.getByText("UNSCHED-WO")).toHaveCount(0); + await expect(page.getByRole("button", { name: /Unscheduled/ })).toHaveCount(0); + + await page.getByRole("tab", { name: "Mon · 08/17" }).click(); + await expect(page.getByRole("tab", { name: "Mon · 08/17" })).toHaveAttribute( + "aria-selected", + "true", + ); + await expect(page.getByText("MON-WO").first()).toBeVisible(); + await expect(page.getByText("WED-WO")).toHaveCount(0); + await expect(page.getByText("UNSCHED-WO")).toHaveCount(0); + }); + + test("updates day tabs and selects Monday when the week changes", async ({ page }) => { + await page.goto("/workorders"); + await expect(page.getByRole("tab", { name: "Wed · 08/19" })).toBeVisible({ timeout: 15_000 }); + + await page.getByRole("button", { name: "Week 8/17–8/21" }).click(); + await page.getByRole("button", { name: /Aug 10/ }).click(); + + await expect(page.getByRole("button", { name: "Week 8/10–8/14" })).toBeVisible(); + await expect(page.getByRole("tab", { name: "Mon · 08/10" })).toHaveAttribute( + "aria-selected", + "true", + ); + await expect(page.getByRole("tab", { name: "Fri · 08/14" })).toBeVisible(); + await expect(page.getByRole("tab", { name: "Mon · 08/17" })).toHaveCount(0); + await expect(page.getByText("PREV-MON-WO").first()).toBeVisible(); + await expect(page.getByText("UNSCHED-WO")).toHaveCount(0); + }); +}); diff --git a/src/app/(auth)/_components/auth-brand-logo.tsx b/src/app/(auth)/_components/auth-brand-logo.tsx new file mode 100644 index 00000000..fd23aac3 --- /dev/null +++ b/src/app/(auth)/_components/auth-brand-logo.tsx @@ -0,0 +1,25 @@ +import { useState } from "react"; + +import { BrandLockup } from "@/components/common/brand-lockup"; +import { cn } from "@/lib/utils"; + +export type AuthBrandLogoProps = { + className?: string; +}; + +export function AuthBrandLogo({ className }: AuthBrandLogoProps) { + const [useFallback, setUseFallback] = useState(false); + + if (useFallback) { + return ; + } + + return ( + Seahaven setUseFallback(true)} + className={cn("mx-auto block h-10 w-auto", className)} + /> + ); +} diff --git a/src/app/(auth)/_components/auth-card-header.tsx b/src/app/(auth)/_components/auth-card-header.tsx index aaf95cf6..51d2b147 100644 --- a/src/app/(auth)/_components/auth-card-header.tsx +++ b/src/app/(auth)/_components/auth-card-header.tsx @@ -11,14 +11,17 @@ export type AuthCardHeaderProps = ComponentPropsWithoutRef<"div"> & { export function AuthCardHeader({ title, subtitle, className, ...props }: AuthCardHeaderProps) { return (
- + {title} {subtitle} diff --git a/src/app/(auth)/login.tsx b/src/app/(auth)/login.tsx index 7d1c4c74..a98e5b3d 100644 --- a/src/app/(auth)/login.tsx +++ b/src/app/(auth)/login.tsx @@ -1,10 +1,10 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { AuthBrandLogo } from "@/app/(auth)/_components/auth-brand-logo"; import { AuthCardHeader } from "@/app/(auth)/_components/auth-card-header"; import { AuthPageShell } from "@/app/(auth)/_components/auth-page-shell"; import { LoginForm } from "@/app/(auth)/_components/login-form"; -import { BrandLockup } from "@/components/common/brand-lockup"; import { Text } from "@/components/ui/text"; import { loginSchema, type LoginFormValues } from "@/domain/auth/schemas/login-schema"; import { useAuthContext } from "@/providers/auth-context"; @@ -27,7 +27,7 @@ export default function LoginPage() { return ( - + { + const isCompletionStatus = isCompletionStatusLabel(status); + if ( + isCompletionStatus && + (!view.isClosabilityReady || getClosabilityGaps(detailToClosabilityInput(wo)).length > 0) + ) { + return; + } + view.statusMutation.mutate(status as WorkOrderStatus); + }; + return ( view.statusMutation.mutate(status)} + onStatusChange={handleStatusChange} onOpenDispatch={() => view.setDispatchOpen(true)} /> diff --git a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx index 7eaf94f3..bdfe8af9 100644 --- a/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx +++ b/src/app/(protected)/workorders/_components/detail/tabs/slide-over-info-tab-edit-view.tsx @@ -59,18 +59,29 @@ export function SlideOverInfoTabEditView({ pocName={draft.pocName} pocPhone={draft.pocPhone} pocNotes={draft.pocNotes} + additionalContacts={draft.additionalContacts} sites={sites} onSave={(patch) => onDraftChange({ site: patch.site, location: patch.location, locationId: patch.locationId, + pocName: patch.pocName, + pocPhone: patch.pocPhone, + pocNotes: patch.pocNotes, + ...(patch.additionalContacts !== undefined + ? { additionalContacts: patch.additionalContacts } + : {}), }) } /> - onDraftChange({ type })} /> + onDraftChange({ type, severity })} + /> void; }; export function WorkOrderStatusMenu({ status, disabled = false, + closability, onStatusChange, }: WorkOrderStatusMenuProps) { const { anchorEl, open, setAnchor, close } = useMenuAnchor(); + const closabilityGaps = getClosabilityGaps(closability); return ( <> @@ -29,21 +38,31 @@ export function WorkOrderStatusMenu({ - {WORK_ORDER_STATUS_OPTIONS.map((option) => ( - { - close(); - if (option !== status) { - onStatusChange(option); + {WORK_ORDER_STATUS_OPTIONS.map((option) => { + const isCompletionStatus = isCompletionStatusLabel(option); + const isCompletedBlocked = isCompletionStatus && closabilityGaps.length > 0; + return ( + - {option} - - ))} + onClick={() => { + if (isCompletedBlocked) { + return; + } + close(); + if (option !== status) { + onStatusChange(option); + } + }} + > + {option} + + ); + })} ); diff --git a/src/app/(protected)/workorders/_components/list/filter-bar/day-tabs.tsx b/src/app/(protected)/workorders/_components/list/filter-bar/day-tabs.tsx new file mode 100644 index 00000000..1745af5a --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/filter-bar/day-tabs.tsx @@ -0,0 +1,35 @@ +import { NavPillButton } from "@/components/ui/nav-pill-button"; +import { formatDayTabLabel, weekdaysMonFri } from "@/domain/work-orders/utils/week-day-nav-utils"; + +type DayTabsProps = { + weekMonday: string; + selectedDayIso: string; + onChange: (dayIso: string) => void; + disabled?: boolean; +}; + +export function DayTabs({ weekMonday, selectedDayIso, onChange, disabled = false }: DayTabsProps) { + const days = weekdaysMonFri(weekMonday); + + return ( +
+ {days.map((iso) => { + const label = formatDayTabLabel(iso); + const active = iso === selectedDayIso; + return ( + onChange(iso)} + > + {label} + + ); + })} +
+ ); +} diff --git a/src/app/(protected)/workorders/_components/list/filter-bar/week-nav.tsx b/src/app/(protected)/workorders/_components/list/filter-bar/week-nav.tsx deleted file mode 100644 index ab8fc786..00000000 --- a/src/app/(protected)/workorders/_components/list/filter-bar/week-nav.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; - -import { ChevronLeft, ChevronRight } from "lucide-react"; - -import { NavArrowButton } from "@/components/ui/nav-arrow-button"; - -import { NavPillButton } from "@/components/ui/nav-pill-button"; - -import { - buildWeekWindow, - isFutureWeek, - isLiveWeek, -} from "@/domain/work-orders/utils/list-week-utils"; - -type WeekNavProps = { - weekMonday: string; - - onChange: (mondayIso: string) => void; - - disabled?: boolean; -}; - -const VISIBLE_COUNT = 3; - -const WINDOW_BEFORE = 12; - -const WINDOW_AFTER = 1; - -function WeekLiveBadge() { - return ( - - Live - - ); -} - -export function WeekNav({ weekMonday, onChange, disabled = false }: WeekNavProps) { - const weeks = useMemo( - () => buildWeekWindow(weekMonday, WINDOW_BEFORE, WINDOW_AFTER), - - [weekMonday], - ); - - const activeIndex = useMemo( - () => weeks.findIndex((week) => week.mondayIso === weekMonday), - - [weeks, weekMonday], - ); - - const [offset, setOffset] = useState(() => Math.max(0, activeIndex >= 0 ? activeIndex - 1 : 0)); - - useEffect(() => { - if (activeIndex < 0) return; - - const maxOffset = Math.max(0, weeks.length - VISIBLE_COUNT); - - setOffset((current) => { - if (activeIndex < current) return Math.min(activeIndex, maxOffset); - - if (activeIndex > current + VISIBLE_COUNT - 1) { - return Math.min(Math.max(0, activeIndex - (VISIBLE_COUNT - 1)), maxOffset); - } - - return current; - }); - }, [activeIndex, weeks.length]); - - const visible = weeks.slice(offset, offset + VISIBLE_COUNT); - - const canGoBack = offset > 0; - - const canGoForward = offset + VISIBLE_COUNT < weeks.length; - - return ( -
- setOffset((current) => current - 1)} - aria-label="Previous weeks" - > - - - - {visible.map((week) => { - const isActive = weekMonday === week.mondayIso; - - const isDisabled = isFutureWeek(week.mondayIso); - - const showLiveBadge = isLiveWeek(week.mondayIso); - - return ( - onChange(week.mondayIso)} - > - {week.label} - - {showLiveBadge && } - - ); - })} - - setOffset((current) => current + 1)} - aria-label="Next weeks" - > - - -
- ); -} diff --git a/src/app/(protected)/workorders/_components/list/filter-bar/week-selector.tsx b/src/app/(protected)/workorders/_components/list/filter-bar/week-selector.tsx new file mode 100644 index 00000000..49f985f4 --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/filter-bar/week-selector.tsx @@ -0,0 +1,81 @@ +import { Popover } from "@mui/material"; +import { ChevronDown } from "lucide-react"; +import { WeekPicker } from "@/app/(protected)/workorders/_components/wizard/week-picker"; +import { MenuAnchorButton } from "@/components/ui/menu-anchor-button"; +import { + compactWeekLabel, + isFutureWeek, + isLiveWeek, +} from "@/domain/work-orders/utils/list-week-utils"; +import { useMenuAnchor } from "@/hooks/use-menu-anchor"; +import { cn } from "@/lib/utils"; + +type WeekSelectorProps = { + weekMonday: string; + onChange: (mondayIso: string) => void; + disabled?: boolean; +}; + +function WeekLiveBadge() { + return ( + + Live + + ); +} + +export function WeekSelector({ weekMonday, onChange, disabled = false }: WeekSelectorProps) { + const { anchorEl, open, setAnchor, close } = useMenuAnchor(); + const live = isLiveWeek(weekMonday); + const label = compactWeekLabel(weekMonday); + + const handlePick = (mondayIso: string) => { + if (isFutureWeek(mondayIso)) { + return; + } + onChange(mondayIso); + close(); + }; + + return ( + <> + + {label} + {Boolean(live) && } + + + +
+ +
+
+ + ); +} diff --git a/src/app/(protected)/workorders/_components/list/filter-bar/work-orders-filter-bar.tsx b/src/app/(protected)/workorders/_components/list/filter-bar/work-orders-filter-bar.tsx index 3a1c114f..3f0bdb13 100644 --- a/src/app/(protected)/workorders/_components/list/filter-bar/work-orders-filter-bar.tsx +++ b/src/app/(protected)/workorders/_components/list/filter-bar/work-orders-filter-bar.tsx @@ -1,12 +1,16 @@ import type { WorkOrderUser } from "@/domain/work-orders/types/work-order"; import type { WorkOrderTypeFilter } from "@/domain/work-orders/types/work-order-list-filters"; +import { DayTabs } from "@/app/(protected)/workorders/_components/list/filter-bar/day-tabs"; import { DispatcherFilter } from "@/app/(protected)/workorders/_components/list/filter-bar/dispatcher-filter"; -import { WeekNav } from "@/app/(protected)/workorders/_components/list/filter-bar/week-nav"; +import { WeekSelector } from "@/app/(protected)/workorders/_components/list/filter-bar/week-selector"; import { WoTypeSegment } from "@/app/(protected)/workorders/_components/list/filter-bar/wo-type-segment"; +import { Text } from "@/components/ui/text"; type WorkOrdersFilterBarProps = { weekMonday: string; onWeekChange: (mondayIso: string) => void; + selectedDayIso: string; + onSelectedDayChange: (dayIso: string) => void; dispatcherIds: Set; onDispatcherChange: (ids: Set) => void; typeFilter: WorkOrderTypeFilter; @@ -22,6 +26,8 @@ type WorkOrdersFilterBarProps = { export function WorkOrdersFilterBar({ weekMonday, onWeekChange, + selectedDayIso, + onSelectedDayChange, dispatcherIds, onDispatcherChange, typeFilter, @@ -37,21 +43,35 @@ export function WorkOrdersFilterBar({ return (
- - - -
- {resultLabel} +
+ +
+ + {resultLabel} + +
+
+ + +
+
+ +
); } 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 b18ef7d7..68605ce8 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 @@ -39,6 +39,7 @@ export function buildInlineDraftCreateInput( pocName: draft.pocName, pocPhone: draft.pocPhone, pocNotes: draft.pocNotes, + additionalContacts: draft.additionalContacts ?? [], scheduleWeekOnly, targetWeek: draft.targetWeek || "", isAddOnOverride: draft.isAddOnOverride, 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 new file mode 100644 index 00000000..3fe930cd --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/table/cells/build-site-dialog-patch.ts @@ -0,0 +1,42 @@ +import { extractLocationLabel } 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 { 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 BuildSiteDialogPatchArgs = { + code: string; + locId: string; + selected: LocationOption | undefined; + pocName: string; + pocPhone: string; + pocNotes: string; + extraContacts: WorkOrderAdditionalContact[]; + contactsDirty: boolean; + baselineHadContacts: boolean; +}; + +export function buildSiteDialogPatch({ + code, + locId, + selected, + pocName, + pocPhone, + pocNotes, + extraContacts, + contactsDirty, + baselineHadContacts, +}: BuildSiteDialogPatchArgs): SitePatch { + const patch: SitePatch = { + site: code || (selected ? resolveSiteCode(selected) : ""), + location: selected ? extractLocationLabel(selected) : "", + locationId: locId, + pocName, + pocPhone, + pocNotes, + }; + if (contactsDirty || baselineHadContacts) { + patch.additionalContacts = extraContacts; + } + return patch; +} 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 89a703f6..104286d5 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 @@ -1,6 +1,7 @@ import { useState } from "react"; import { MapPin } from "lucide-react"; import { formatSitePlace } from "@/domain/locations/mappers/location-mapper"; +import type { WorkOrderAdditionalContact } from "@/domain/work-orders/types/work-order-additional-contact"; import type { LocationOption } from "@/domain/work-orders/types/work-order"; import { HighlightText } from "./highlight-text"; import { HoverChevron } from "./hover-chevron"; @@ -15,6 +16,7 @@ type SiteCellProps = { pocName?: string; pocPhone?: string; pocNotes?: string; + additionalContacts?: WorkOrderAdditionalContact[]; sites: LocationOption[]; q?: string; /** Inline create: editable POC + Site/POC required to confirm the modal. */ @@ -31,6 +33,7 @@ export function SiteCell({ pocName, pocPhone, pocNotes, + additionalContacts, sites, q, createMode = false, @@ -93,6 +96,7 @@ export function SiteCell({ pocName={pocName} pocPhone={pocPhone} pocNotes={pocNotes} + additionalContacts={additionalContacts} sites={sites} createMode={createMode} onSave={onSave} diff --git a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-footer.tsx b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-footer.tsx new file mode 100644 index 00000000..ed269746 --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-footer.tsx @@ -0,0 +1,19 @@ +import { Button } from "@/components/ui/button"; + +type SiteDialogFooterProps = { + onClose: () => void; + onConfirm: () => void; +}; + +export function SiteDialogFooter({ onClose, onConfirm }: SiteDialogFooterProps) { + return ( +
+ + +
+ ); +} diff --git a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-poc-fields.tsx b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-poc-fields.tsx index 61ca8f78..66874d72 100644 --- a/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-poc-fields.tsx +++ b/src/app/(protected)/workorders/_components/list/table/cells/site-dialog-poc-fields.tsx @@ -1,87 +1 @@ -import { - DLG_INPUT_CLS, - DLG_INPUT_STYLE, -} from "@/app/(protected)/workorders/_components/wizard/wizard-input-styles"; -import { Text } from "@/components/ui/text"; -import { FieldLabel } from "@/app/(protected)/workorders/_components/list/table/cells/dialog-field-label"; - -type SiteDialogPocFieldsProps = { - pocName: string; - pocPhone: string; - pocNotes: string; - editable?: boolean; - showErrors?: boolean; - onPocNameChange?: (value: string) => void; - onPocPhoneChange?: (value: string) => void; - onPocNotesChange?: (value: string) => void; -}; - -export function SiteDialogPocFields({ - pocName, - pocPhone, - pocNotes, - editable = false, - showErrors = false, - onPocNameChange, - onPocPhoneChange, - onPocNotesChange, -}: SiteDialogPocFieldsProps) { - const nameMissing = showErrors && !pocName.trim(); - const phoneMissing = showErrors && !pocPhone.trim(); - - return ( -
- Point of Contact - - {editable - ? "Auto-filled from records — edit if outdated" - : "Shown from site/work order records. Board edit saves site code only."} - -
-
- onPocNameChange?.(e.target.value)} - /> - - Contact name is required - -
-
- onPocPhoneChange?.(e.target.value)} - /> - - Contact phone is required - -
-