mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 06:13:13 +00:00
refactor: align work order wizard with frontend standards
This commit is contained in:
parent
caecc05b68
commit
2f5ee30020
6 changed files with 329 additions and 266 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Dialog } from "@mui/material";
|
||||
import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { useDropdownOptionsByCategory } from "@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-category";
|
||||
import { mapWizardDraftToFormValues } from "@/domain/work-orders/mappers/wizard-draft-mapper";
|
||||
import { isWoWizardMockEnabled } from "@/domain/work-orders/mocks/is-wo-wizard-mock-enabled";
|
||||
|
|
@ -35,6 +36,46 @@ type NewWoWizardProps = {
|
|||
users: WorkOrderUser[];
|
||||
};
|
||||
|
||||
function WizardStepHeader({ step }: { step: number }) {
|
||||
return (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "0.08em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--color-primary)",
|
||||
}}
|
||||
>
|
||||
Step {step} of {WIZARD_STEPS.length}
|
||||
</div>
|
||||
<Text
|
||||
variant="title"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
fontSize: 20,
|
||||
color: "var(--foreground)",
|
||||
marginTop: "6px",
|
||||
}}
|
||||
>
|
||||
{WIZARD_STEPS[step - 1].title}
|
||||
</Text>
|
||||
<Text
|
||||
variant="description"
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
color: "var(--muted-foreground)",
|
||||
marginTop: "4px",
|
||||
}}
|
||||
>
|
||||
{WIZARD_STEPS[step - 1].subtitle}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function NewWoWizard({ open, onOpenChange, users }: NewWoWizardProps) {
|
||||
const mockFallback = isWoWizardMockEnabled();
|
||||
const [step, setStep] = useState(1);
|
||||
|
|
@ -109,9 +150,7 @@ export function NewWoWizard({ open, onOpenChange, users }: NewWoWizardProps) {
|
|||
}
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
createMutation.mutate(mapWizardDraftToFormValues(draft));
|
||||
};
|
||||
const handleCreate = () => createMutation.mutate(mapWizardDraftToFormValues(draft));
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
|
@ -141,41 +180,7 @@ export function NewWoWizard({ open, onOpenChange, users }: NewWoWizardProps) {
|
|||
className="min-h-0 flex-1 overflow-y-auto"
|
||||
style={{ padding: 32, background: "var(--color-bg-surface)" }}
|
||||
>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "0.08em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--color-primary)",
|
||||
}}
|
||||
>
|
||||
Step {step} of {WIZARD_STEPS.length}
|
||||
</div>
|
||||
<h2
|
||||
style={{
|
||||
fontFamily: "var(--font-display)",
|
||||
fontWeight: 700,
|
||||
fontSize: 20,
|
||||
color: "var(--foreground)",
|
||||
marginTop: 6,
|
||||
}}
|
||||
>
|
||||
{WIZARD_STEPS[step - 1].title}
|
||||
</h2>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "var(--font-sans)",
|
||||
fontSize: 13,
|
||||
color: "var(--muted-foreground)",
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{WIZARD_STEPS[step - 1].subtitle}
|
||||
</p>
|
||||
</div>
|
||||
<WizardStepHeader step={step} />
|
||||
|
||||
{step === 1 && (
|
||||
<WizardStepTypeSchedule
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export function WizardDateField({
|
|||
},
|
||||
}}
|
||||
>
|
||||
{allowWeekMode && (
|
||||
{Boolean(allowWeekMode) && (
|
||||
<div className="flex items-center gap-1.5" style={{ marginBottom: 10 }}>
|
||||
{(["week", "date"] as const).map((m) => {
|
||||
const activeMode = cellMode === m;
|
||||
|
|
|
|||
|
|
@ -21,39 +21,6 @@ type WizardSidebarProps = {
|
|||
};
|
||||
|
||||
export function WizardSidebar({ step, draft }: WizardSidebarProps) {
|
||||
const summary: [string, string][] = [
|
||||
["WO #", draft.woNumber ? `#${draft.woNumber}` : "—"],
|
||||
["Type", draft.type || "—"],
|
||||
[
|
||||
"Schedule on",
|
||||
draft.scheduledOn
|
||||
? draft.scheduleWeekOnly
|
||||
? fmtWeekRange(draft.scheduledOn)
|
||||
: fmtDate(draft.scheduledOn)
|
||||
: "—",
|
||||
],
|
||||
["Due date", fmtDate(draft.dueDate) || "—"],
|
||||
["Assigned to", draft.assigneeName || "—"],
|
||||
["Site", draft.locationName || "—"],
|
||||
[
|
||||
"POC",
|
||||
draft.pocName
|
||||
? draft.pocPhone
|
||||
? `${draft.pocName} · ${draft.pocPhone}`
|
||||
: draft.pocName
|
||||
: "—",
|
||||
],
|
||||
["Service", draft.pm || "—"],
|
||||
["Technician", draft.vendorName || "—"],
|
||||
["Company", draft.vendorName || "—"],
|
||||
["Appt time", draft.apptStart ? formatApptRange(draft.apptStart, draft.apptEnd) : "—"],
|
||||
["Status", draft.status || "Incomplete"],
|
||||
];
|
||||
|
||||
const statusLabel = draft.status || "Incomplete";
|
||||
const statusValue = statusLabel as WizardWOStatus;
|
||||
const statusDotColor = WIZARD_STATUS_DOTS[statusValue] ?? WIZARD_STATUS_DOTS.Incomplete;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex shrink-0 flex-col overflow-y-auto"
|
||||
|
|
@ -149,56 +116,83 @@ export function WizardSidebar({ step, draft }: WizardSidebarProps) {
|
|||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "auto", paddingTop: 44 }}>
|
||||
<WizardSidebarSummary draft={draft} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WizardSidebarSummary({ draft }: { draft: WorkOrderWizardDraft }) {
|
||||
const summary: [string, string][] = [
|
||||
["WO #", draft.woNumber ? `#${draft.woNumber}` : "—"],
|
||||
["Type", draft.type || "—"],
|
||||
[
|
||||
"Schedule on",
|
||||
draft.scheduledOn
|
||||
? draft.scheduleWeekOnly
|
||||
? fmtWeekRange(draft.scheduledOn)
|
||||
: fmtDate(draft.scheduledOn)
|
||||
: "—",
|
||||
],
|
||||
["Due date", fmtDate(draft.dueDate) || "—"],
|
||||
["Assigned to", draft.assigneeName || "—"],
|
||||
["Site", draft.locationName || "—"],
|
||||
[
|
||||
"POC",
|
||||
draft.pocName
|
||||
? draft.pocPhone
|
||||
? `${draft.pocName} · ${draft.pocPhone}`
|
||||
: draft.pocName
|
||||
: "—",
|
||||
],
|
||||
["Service", draft.pm || "—"],
|
||||
["Technician", draft.vendorName || "—"],
|
||||
["Company", draft.vendorName || "—"],
|
||||
["Appt time", draft.apptStart ? formatApptRange(draft.apptStart, draft.apptEnd) : "—"],
|
||||
["Status", draft.status || "Incomplete"],
|
||||
];
|
||||
|
||||
const statusLabel = draft.status || "Incomplete";
|
||||
const statusValue = statusLabel as WizardWOStatus;
|
||||
const statusDotColor = WIZARD_STATUS_DOTS[statusValue] ?? WIZARD_STATUS_DOTS.Incomplete;
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: "auto", paddingTop: 44 }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "0.08em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--muted-foreground)",
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
This work order
|
||||
</div>
|
||||
{summary.map(([label, val], i) => (
|
||||
<div
|
||||
key={label}
|
||||
className="flex items-center justify-between gap-2"
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "0.08em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--muted-foreground)",
|
||||
marginBottom: 8,
|
||||
padding: "4px 0",
|
||||
borderBottom: i < summary.length - 1 ? "1px solid var(--color-border)" : "none",
|
||||
}}
|
||||
>
|
||||
This work order
|
||||
</div>
|
||||
{summary.map(([label, val], i) => (
|
||||
<div
|
||||
key={label}
|
||||
className="flex items-center justify-between gap-2"
|
||||
<span
|
||||
className="shrink-0"
|
||||
style={{
|
||||
padding: "4px 0",
|
||||
borderBottom: i < summary.length - 1 ? "1px solid var(--color-border)" : "none",
|
||||
fontFamily: "var(--font-sans)",
|
||||
fontSize: 11,
|
||||
fontWeight: 400,
|
||||
color: "var(--muted-foreground)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="shrink-0"
|
||||
style={{
|
||||
fontFamily: "var(--font-sans)",
|
||||
fontSize: 11,
|
||||
fontWeight: 400,
|
||||
color: "var(--muted-foreground)",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
{label === "Status" ? (
|
||||
<span className="inline-flex min-w-0 items-center gap-1.5">
|
||||
<ColorDot color={statusDotColor} />
|
||||
<span
|
||||
className="truncate text-right"
|
||||
style={{
|
||||
fontFamily: "var(--font-sans)",
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: "var(--foreground)",
|
||||
}}
|
||||
>
|
||||
{statusLabel}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
{label}
|
||||
</span>
|
||||
{label === "Status" ? (
|
||||
<span className="inline-flex min-w-0 items-center gap-1.5">
|
||||
<ColorDot color={statusDotColor} />
|
||||
<span
|
||||
className="truncate text-right"
|
||||
style={{
|
||||
|
|
@ -208,12 +202,24 @@ export function WizardSidebar({ step, draft }: WizardSidebarProps) {
|
|||
color: "var(--foreground)",
|
||||
}}
|
||||
>
|
||||
{val}
|
||||
{statusLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className="truncate text-right"
|
||||
style={{
|
||||
fontFamily: "var(--font-sans)",
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: "var(--foreground)",
|
||||
}}
|
||||
>
|
||||
{val}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ExternalLink, Info, Mail, MapPin, Phone } from "lucide-react";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import type { LocationOption } from "@/domain/work-orders/types/work-order";
|
||||
import type { Location } from "@/domain/locations/types/location";
|
||||
import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard";
|
||||
|
|
@ -63,16 +64,18 @@ export function WizardStepLocationService({
|
|||
}}
|
||||
/>
|
||||
{!draft.locationId && (
|
||||
<p
|
||||
<Text
|
||||
as="p"
|
||||
variant="description"
|
||||
className="mt-1.5 flex items-center gap-1"
|
||||
style={{ fontSize: 11, color: "var(--muted-foreground)" }}
|
||||
sx={{ fontSize: 11, color: "var(--muted-foreground)" }}
|
||||
>
|
||||
<Info size={12} className="shrink-0" /> Site is required to continue.
|
||||
</p>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{locationDetail && draft.locationId && (
|
||||
{locationDetail != null && draft.locationId !== "" && (
|
||||
<>
|
||||
<div
|
||||
className="space-y-1.5 rounded-lg p-3"
|
||||
|
|
@ -91,11 +94,13 @@ export function WizardStepLocationService({
|
|||
.join(", ") || "—"
|
||||
}
|
||||
/>
|
||||
{locationDetail.phone && <Detail icon={Phone} text={locationDetail.phone} />}
|
||||
{locationDetail.contactEmail && (
|
||||
<Detail icon={Mail} text={locationDetail.contactEmail} />
|
||||
{Boolean(locationDetail.phone) && (
|
||||
<Detail icon={Phone} text={locationDetail.phone ?? ""} />
|
||||
)}
|
||||
{locationDetail.address && (
|
||||
{Boolean(locationDetail.contactEmail) && (
|
||||
<Detail icon={Mail} text={locationDetail.contactEmail ?? ""} />
|
||||
)}
|
||||
{Boolean(locationDetail.address) && (
|
||||
<a
|
||||
href={buildMapsUrl(locationDetail)}
|
||||
target="_blank"
|
||||
|
|
@ -109,9 +114,13 @@ export function WizardStepLocationService({
|
|||
</div>
|
||||
<div>
|
||||
<WizSubLabel>Point of contact</WizSubLabel>
|
||||
<p style={{ fontSize: 11.5, color: "var(--muted-foreground)", margin: "2px 0 8px" }}>
|
||||
<Text
|
||||
as="p"
|
||||
variant="description"
|
||||
sx={{ fontSize: 11.5, color: "var(--muted-foreground)", margin: "2px 0 8px" }}
|
||||
>
|
||||
Auto-filled from records — edit if outdated
|
||||
</p>
|
||||
</Text>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<input
|
||||
value={draft.pocName}
|
||||
|
|
@ -158,9 +167,13 @@ export function WizardStepLocationService({
|
|||
</div>
|
||||
|
||||
{!draft.locationId && !draft.pm && (
|
||||
<p style={{ fontSize: 12, fontStyle: "italic", color: "var(--muted-foreground)" }}>
|
||||
<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.
|
||||
</p>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import type { WorkOrderWizardDraft } from "@/domain/work-orders/types/work-order-wizard";
|
||||
import { WO_TYPE_META, WO_TYPES, type WOType } from "@/domain/work-orders/types/work-order-wizard";
|
||||
import { WizardDateField } from "@/app/(protected)/workorders/_components/wizard/wizard-date-field";
|
||||
|
|
@ -23,6 +24,93 @@ const WO_TYPE_ICONS: Record<WOType, ElementType> = {
|
|||
Overdue: Clock,
|
||||
};
|
||||
|
||||
function WoTypePicker({
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
selected: WOType | "";
|
||||
onSelect: (type: WOType) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="auto-rows-fr grid grid-cols-2 gap-3">
|
||||
{WO_TYPES.map((t) => {
|
||||
const isSelected = selected === t;
|
||||
const meta = WO_TYPE_META[t];
|
||||
const Icon = WO_TYPE_ICONS[t];
|
||||
return (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
onClick={() => onSelect(t)}
|
||||
className="relative flex items-start gap-2.5 text-left transition-colors"
|
||||
style={{
|
||||
border: isSelected
|
||||
? "2px solid var(--color-primary)"
|
||||
: "1px solid var(--color-border)",
|
||||
borderRadius: 8,
|
||||
background: isSelected ? "var(--color-primary-tint)" : "var(--color-bg-surface)",
|
||||
padding: isSelected ? "13px 15px" : "14px 16px",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isSelected) {
|
||||
const b = e.currentTarget;
|
||||
b.style.borderColor = "color-mix(in srgb, var(--color-primary) 40%, transparent)";
|
||||
b.style.background = "var(--color-bg-muted)";
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isSelected) {
|
||||
const b = e.currentTarget;
|
||||
b.style.borderColor = "var(--color-border)";
|
||||
b.style.background = "var(--color-bg-surface)";
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="flex shrink-0 items-center justify-center"
|
||||
style={{ width: 30, height: 30, borderRadius: 8, background: meta.bg }}
|
||||
>
|
||||
<Icon size={16} style={{ color: meta.fg }} />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "var(--font-sans)",
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "var(--foreground)",
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</div>
|
||||
<Text
|
||||
as="p"
|
||||
variant="description"
|
||||
sx={{
|
||||
fontSize: 11,
|
||||
fontWeight: 400,
|
||||
color: "var(--muted-foreground)",
|
||||
marginTop: "2px",
|
||||
lineHeight: 1.35,
|
||||
}}
|
||||
>
|
||||
{meta.desc}
|
||||
</Text>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<CheckCircle2
|
||||
size={14}
|
||||
className="absolute"
|
||||
style={{ top: 8, right: 8, color: "var(--color-primary)" }}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type WizardStepTypeScheduleProps = {
|
||||
draft: WorkOrderWizardDraft;
|
||||
users: WorkOrderUser[];
|
||||
|
|
@ -40,82 +128,7 @@ export function WizardStepTypeSchedule({
|
|||
<div className="space-y-6">
|
||||
<div>
|
||||
<WizLabel required>Type of WO</WizLabel>
|
||||
<div className="auto-rows-fr grid grid-cols-2 gap-3">
|
||||
{WO_TYPES.map((t) => {
|
||||
const selected = draft.type === t;
|
||||
const meta = WO_TYPE_META[t];
|
||||
const Icon = WO_TYPE_ICONS[t];
|
||||
return (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
onClick={() => onUpdate("type", t)}
|
||||
className="relative flex items-start gap-2.5 text-left transition-colors"
|
||||
style={{
|
||||
border: selected
|
||||
? "2px solid var(--color-primary)"
|
||||
: "1px solid var(--color-border)",
|
||||
borderRadius: 8,
|
||||
background: selected ? "var(--color-primary-tint)" : "var(--color-bg-surface)",
|
||||
padding: selected ? "13px 15px" : "14px 16px",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!selected) {
|
||||
const b = e.currentTarget;
|
||||
b.style.borderColor =
|
||||
"color-mix(in srgb, var(--color-primary) 40%, transparent)";
|
||||
b.style.background = "var(--color-bg-muted)";
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!selected) {
|
||||
const b = e.currentTarget;
|
||||
b.style.borderColor = "var(--color-border)";
|
||||
b.style.background = "var(--color-bg-surface)";
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="flex shrink-0 items-center justify-center"
|
||||
style={{ width: 30, height: 30, borderRadius: 8, background: meta.bg }}
|
||||
>
|
||||
<Icon size={16} style={{ color: meta.fg }} />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "var(--font-sans)",
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "var(--foreground)",
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "var(--font-sans)",
|
||||
fontSize: 11,
|
||||
fontWeight: 400,
|
||||
color: "var(--muted-foreground)",
|
||||
marginTop: 2,
|
||||
lineHeight: 1.35,
|
||||
}}
|
||||
>
|
||||
{meta.desc}
|
||||
</p>
|
||||
</div>
|
||||
{selected && (
|
||||
<CheckCircle2
|
||||
size={14}
|
||||
className="absolute"
|
||||
style={{ top: 8, right: 8, color: "var(--color-primary)" }}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<WoTypePicker selected={draft.type} onSelect={(t) => onUpdate("type", t)} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Clock, ExternalLink, Mail, Wrench } from "lucide-react";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
|
||||
import {
|
||||
ALL_WIZARD_STATUSES,
|
||||
|
|
@ -28,6 +29,70 @@ type WizardStepVendorTimeProps = {
|
|||
onPatch: (patch: Partial<WorkOrderWizardDraft>) => void;
|
||||
};
|
||||
|
||||
function WizardVendorDetail({
|
||||
vendor,
|
||||
techPhone,
|
||||
vendorNotes,
|
||||
onUpdate,
|
||||
}: {
|
||||
vendor: VendorDropdownItem;
|
||||
techPhone: string;
|
||||
vendorNotes: string;
|
||||
onUpdate: <K extends keyof WorkOrderWizardDraft>(key: K, value: WorkOrderWizardDraft[K]) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<WizSubLabel>Technician phone</WizSubLabel>
|
||||
<input
|
||||
value={techPhone}
|
||||
onChange={(e) => onUpdate("techPhone", e.target.value)}
|
||||
placeholder="(000) 000-0000"
|
||||
className={`${DLG_INPUT_CLS} mt-2`}
|
||||
style={DLG_INPUT_STYLE}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<WizSubLabel>Company</WizSubLabel>
|
||||
<div
|
||||
className="mt-2 space-y-1.5 rounded-lg p-3"
|
||||
style={{
|
||||
background: "var(--color-bg-muted)",
|
||||
border: "1px solid var(--color-border)",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: "var(--foreground)" }}>
|
||||
{vendor.companyName || "—"}
|
||||
</div>
|
||||
{Boolean(vendor.address) && <Detail icon={Mail} text={vendor.address} />}
|
||||
{Boolean(vendor.address) && (
|
||||
<a
|
||||
href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(vendor.address)}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-[12px] hover:underline"
|
||||
style={{ color: "var(--color-primary)" }}
|
||||
>
|
||||
<ExternalLink size={12} /> Open in Google Maps
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<WizSubLabel>Notes</WizSubLabel>
|
||||
<textarea
|
||||
value={vendorNotes}
|
||||
onChange={(e) => onUpdate("vendorNotes", e.target.value)}
|
||||
placeholder="Contact preferences, availability, etc."
|
||||
rows={2}
|
||||
className={`${DLG_INPUT_CLS} mt-2 resize-none`}
|
||||
style={{ ...DLG_INPUT_STYLE, fontFamily: "inherit" }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function WizardStepVendorTime({
|
||||
draft,
|
||||
vendors,
|
||||
|
|
@ -90,56 +155,13 @@ export function WizardStepVendorTime({
|
|||
/>
|
||||
</div>
|
||||
|
||||
{draft.vendorId && selectedVendor && (
|
||||
<>
|
||||
<div>
|
||||
<WizSubLabel>Technician phone</WizSubLabel>
|
||||
<input
|
||||
value={draft.techPhone}
|
||||
onChange={(e) => onUpdate("techPhone", e.target.value)}
|
||||
placeholder="(000) 000-0000"
|
||||
className={`${DLG_INPUT_CLS} mt-2`}
|
||||
style={DLG_INPUT_STYLE}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<WizSubLabel>Company</WizSubLabel>
|
||||
<div
|
||||
className="mt-2 space-y-1.5 rounded-lg p-3"
|
||||
style={{
|
||||
background: "var(--color-bg-muted)",
|
||||
border: "1px solid var(--color-border)",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: "var(--foreground)" }}>
|
||||
{selectedVendor.companyName || "—"}
|
||||
</div>
|
||||
{selectedVendor.address && <Detail icon={Mail} text={selectedVendor.address} />}
|
||||
{selectedVendor.address && (
|
||||
<a
|
||||
href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(selectedVendor.address)}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-[12px] hover:underline"
|
||||
style={{ color: "var(--color-primary)" }}
|
||||
>
|
||||
<ExternalLink size={12} /> Open in Google Maps
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<WizSubLabel>Notes</WizSubLabel>
|
||||
<textarea
|
||||
value={draft.vendorNotes}
|
||||
onChange={(e) => onUpdate("vendorNotes", e.target.value)}
|
||||
placeholder="Contact preferences, availability, etc."
|
||||
rows={2}
|
||||
className={`${DLG_INPUT_CLS} mt-2 resize-none`}
|
||||
style={{ ...DLG_INPUT_STYLE, fontFamily: "inherit" }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
{draft.vendorId !== "" && selectedVendor != null && (
|
||||
<WizardVendorDetail
|
||||
vendor={selectedVendor}
|
||||
techPhone={draft.techPhone}
|
||||
vendorNotes={draft.vendorNotes}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div>
|
||||
|
|
@ -178,14 +200,18 @@ export function WizardStepVendorTime({
|
|||
onPick={(t) => onUpdate("apptEnd", t)}
|
||||
/>
|
||||
</div>
|
||||
{draft.apptStart && draft.apptEnd && calcDuration(draft.apptStart, draft.apptEnd) && (
|
||||
<p
|
||||
className="mt-2 flex items-center gap-1.5"
|
||||
style={{ fontSize: 11.5, color: "var(--muted-foreground)" }}
|
||||
>
|
||||
<Clock size={12} /> Duration {calcDuration(draft.apptStart, draft.apptEnd)}
|
||||
</p>
|
||||
)}
|
||||
{draft.apptStart !== "" &&
|
||||
draft.apptEnd !== "" &&
|
||||
Boolean(calcDuration(draft.apptStart, draft.apptEnd)) && (
|
||||
<Text
|
||||
as="p"
|
||||
variant="caption"
|
||||
className="mt-2 flex items-center gap-1.5"
|
||||
sx={{ fontSize: 11.5, color: "var(--muted-foreground)" }}
|
||||
>
|
||||
<Clock size={12} /> Duration {calcDuration(draft.apptStart, draft.apptEnd)}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue