From bf61508fe47a44650a3b1d5b71d2968116fbfba8 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Fri, 17 Jul 2026 10:48:17 -0300 Subject: [PATCH] feat(work-orders): board wizard create with sites and service notes --- .../_components/wizard/mini-calendar.tsx | 69 ++- .../_components/wizard/new-wo-wizard.tsx | 99 ++-- .../_components/wizard/wizard-date-field.tsx | 136 +++++- .../wizard/wizard-field-select.tsx | 41 +- .../wizard/wizard-service-notes-fields.tsx | 140 ++++++ .../_components/wizard/wizard-sidebar.tsx | 15 +- .../wizard/wizard-step-location-service.tsx | 177 +++++-- .../wizard/wizard-step-type-schedule.tsx | 60 ++- .../wizard/wizard-step-vendor-time.tsx | 33 +- .../_components/work-order-form.tsx | 435 +++++++++--------- 10 files changed, 844 insertions(+), 361 deletions(-) create mode 100644 src/app/(protected)/workorders/_components/wizard/wizard-service-notes-fields.tsx diff --git a/src/app/(protected)/workorders/_components/wizard/mini-calendar.tsx b/src/app/(protected)/workorders/_components/wizard/mini-calendar.tsx index 4c4132fd..4b6ad5d2 100644 --- a/src/app/(protected)/workorders/_components/wizard/mini-calendar.tsx +++ b/src/app/(protected)/workorders/_components/wizard/mini-calendar.tsx @@ -7,10 +7,21 @@ const WEEKDAY_HEADER = ["S", "M", "T", "W", "T", "F", "S"]; type MiniCalendarProps = { value: string; onChange: (iso: string) => void; + /** Range-selection mode — parent owns the click logic. */ + range?: boolean; + rangeStart?: string; + rangeEnd?: string; }; -export function MiniCalendar({ value, onChange }: MiniCalendarProps) { - const init = /^\d{4}-\d{2}-\d{2}$/.test(value) ? value : todayIso(); +export function MiniCalendar({ + value, + onChange, + range, + rangeStart = "", + rangeEnd = "", +}: MiniCalendarProps) { + const initRaw = range ? rangeStart || value : value; + const init = /^\d{4}-\d{2}-\d{2}$/.test(initRaw) ? initRaw : todayIso(); const [iy, im] = init.split("-").map(Number); const [viewY, setViewY] = useState(iy); const [viewM, setViewM] = useState(im); @@ -81,8 +92,46 @@ export function MiniCalendar({ value, onChange }: MiniCalendarProps) { return ; } const iso = `${viewY}-${pad2(viewM)}-${pad2(day)}`; - const selected = iso === value; const isToday = iso === today; + + let bg = "transparent"; + let fg: string = isToday ? "var(--color-primary)" : "var(--color-text-primary)"; + let radius: number | string = 4; + let filled = false; + + if (range && rangeStart) { + const lo = !rangeEnd ? rangeStart : rangeStart <= rangeEnd ? rangeStart : rangeEnd; + const hi = !rangeEnd ? rangeStart : rangeStart <= rangeEnd ? rangeEnd : rangeStart; + if (lo === hi) { + if (iso === lo) { + filled = true; + bg = "var(--color-primary)"; + fg = "var(--color-primary-foreground)"; + radius = 4; + } + } else if (iso === lo) { + filled = true; + bg = "var(--color-primary)"; + fg = "var(--color-primary-foreground)"; + radius = "4px 0 0 4px"; + } else if (iso === hi) { + filled = true; + bg = "var(--color-primary)"; + fg = "var(--color-primary-foreground)"; + radius = "0 4px 4px 0"; + } else if (iso > lo && iso < hi) { + filled = true; + bg = "var(--color-primary-tint)"; + fg = "var(--color-primary)"; + radius = 0; + } + } else if (iso === value) { + filled = true; + bg = "var(--color-primary)"; + fg = "var(--color-primary-foreground)"; + radius = 4; + } + return ( ); })} @@ -130,6 +196,52 @@ export function WizardDateField({ close(); }} /> + ) : allowRange && cellMode === "range" ? ( + <> + + {rangeStart && ( +
+ {(() => { + const n = rangeDayCount(rangeStart, rangeEnd || rangeStart); + return `${n} ${n === 1 ? "day" : "days"} selected`; + })()} +
+ )} + + ) : ( o.key === value); const filtered = q - ? options.filter( - (o) => - o.label.toLowerCase().includes(q.toLowerCase()) || - o.key.toLowerCase().includes(q.toLowerCase()), - ) + ? options.filter((o) => { + const needle = q.toLowerCase(); + return ( + o.label.toLowerCase().includes(needle) || + o.key.toLowerCase().includes(needle) || + (o.description?.toLowerCase().includes(needle) ?? false) + ); + }) : options; const handleClose = () => { @@ -73,9 +78,8 @@ export function WizardFieldSelect({ }} aria-haspopup="listbox" > - {icon != null && {icon}} - {icon == null && selected?.left != null && ( - {selected.left} + {(selected?.left ?? icon) != null && ( + {selected?.left ?? icon} )} {selected ? ( {o.left != null && {o.left}} - - {o.label} + + + {o.label} + + {o.description ? ( + + {o.description} + + ) : null} + {o.right != null && {o.right}} {value === o.key && ( )} diff --git a/src/app/(protected)/workorders/_components/wizard/wizard-service-notes-fields.tsx b/src/app/(protected)/workorders/_components/wizard/wizard-service-notes-fields.tsx new file mode 100644 index 00000000..505a2f7c --- /dev/null +++ b/src/app/(protected)/workorders/_components/wizard/wizard-service-notes-fields.tsx @@ -0,0 +1,140 @@ +import type { ReactNode } from "react"; +import { Check } from "lucide-react"; +import { getPmTypeIcon } from "@/app/(protected)/workorders/_components/list/table/pm-type-icons"; +import { + DLG_INPUT_CLS, + DLG_INPUT_STYLE, +} from "@/app/(protected)/workorders/_components/wizard/wizard-input-styles"; + +type WizardServiceNotesFieldsProps = { + services: { id: string | number; value: string }[]; + sel: string[]; + onToggle: (service: string) => void; + note: string; + setNote: (v: string) => void; + search: string; + setSearch: (v: string) => void; +}; + +function SvcSectionLabel({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +/** Multi-select services + notes — mirrors design ServiceNotesFields (wizard inline). */ +export function WizardServiceNotesFields({ + services, + sel, + onToggle, + note, + setNote, + search, + setSearch, +}: WizardServiceNotesFieldsProps) { + const filtered = search + ? services.filter((s) => s.value.toLowerCase().includes(search.toLowerCase())) + : services; + + return ( + <> +
+ Services + setSearch(e.target.value)} + placeholder="Search services…" + className="mb-1.5 w-full rounded px-2 py-1.5 text-[12px] focus:outline-none focus:ring-1" + style={{ + border: "1px solid var(--color-border)", + color: "var(--color-text-primary)", + background: "var(--color-bg-surface)", + }} + /> +
+ {filtered.map((s) => { + const checked = sel.includes(s.value); + const isPrimary = checked && sel[0] === s.value; + const Icon = getPmTypeIcon(s.value); + return ( + + ); + })} + {filtered.length === 0 && ( +
+ No services match. +
+ )} +
+
+
+ Notes / Issue Detail +