mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 09:13:11 +00:00
fix(work-orders): extract helpers to satisfy governance complexity
This commit is contained in:
parent
116f65ca48
commit
99c758872a
3 changed files with 257 additions and 122 deletions
|
|
@ -32,6 +32,151 @@ type SlideOverInfoTabDisplayViewProps = {
|
|||
pmTypes: string[];
|
||||
};
|
||||
|
||||
type FrozenPoc = ReturnType<typeof effectivePoc>;
|
||||
|
||||
function formatScheduleOn(source: WorkOrderTableRow): string {
|
||||
if (source.scheduleWeekOnly && source.targetWeek) {
|
||||
return `Week of ${source.targetWeek}`;
|
||||
}
|
||||
return source.scheduledOn || "—";
|
||||
}
|
||||
|
||||
function formatApptDisplay(apptTime: string): string {
|
||||
const appt = parseApptTime(apptTime);
|
||||
if (appt.start) return formatApptRange(appt.start, appt.end);
|
||||
return apptTime || "—";
|
||||
}
|
||||
|
||||
function formatPocLine(poc: FrozenPoc): string {
|
||||
const primary = poc.contacts[0];
|
||||
const name = primary?.name || "—";
|
||||
if (!primary?.phone) return name;
|
||||
return `${name} · ${primary.phone}`;
|
||||
}
|
||||
|
||||
function SlideOverTypeField({ type }: { type: WorkOrderTableRow["type"] }) {
|
||||
if (!type) {
|
||||
return <span style={{ color: "var(--color-text-muted)" }}>—</span>;
|
||||
}
|
||||
return <WoTableTypeLabel type={type as WOType} />;
|
||||
}
|
||||
|
||||
function SlideOverCompletedSite({
|
||||
source,
|
||||
poc,
|
||||
sites,
|
||||
}: {
|
||||
source: WorkOrderTableRow;
|
||||
poc: FrozenPoc;
|
||||
sites: LocationOption[];
|
||||
}) {
|
||||
const primaryPoc = poc.contacts[0];
|
||||
return (
|
||||
<SiteCell
|
||||
value={source.site}
|
||||
location={source.location}
|
||||
locationId={source.locationId}
|
||||
pocName={primaryPoc?.name ?? ""}
|
||||
pocPhone={primaryPoc?.phone ?? ""}
|
||||
pocNotes={poc.notes ?? ""}
|
||||
additionalContacts={poc.contacts.slice(1).map((contact) => ({
|
||||
name: contact.name ?? "",
|
||||
phone: contact.phone ?? "",
|
||||
}))}
|
||||
sites={sites}
|
||||
viewOnly
|
||||
frozenSite={source.frozenSite}
|
||||
onSave={() => undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SlideOverSiteField({
|
||||
source,
|
||||
isCompleted,
|
||||
poc,
|
||||
sites,
|
||||
}: {
|
||||
source: WorkOrderTableRow;
|
||||
isCompleted: boolean;
|
||||
poc: FrozenPoc;
|
||||
sites: LocationOption[];
|
||||
}) {
|
||||
if (!isCompleted) {
|
||||
return (
|
||||
<span className="font-mono text-[12.5px] font-bold" style={{ color: "var(--foreground)" }}>
|
||||
{source.site || "—"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <SlideOverCompletedSite source={source} poc={poc} sites={sites} />;
|
||||
}
|
||||
|
||||
function SlideOverOpenService({ source }: { source: WorkOrderTableRow }) {
|
||||
if (!source.pm) {
|
||||
return <span style={{ color: "var(--color-text-muted)" }}>—</span>;
|
||||
}
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 text-[12.5px]">
|
||||
<PmTypeIcon pmType={source.pm} size={14} />
|
||||
{source.pm}
|
||||
{Boolean(source.avetaRequired) && (
|
||||
<AvetaBadge size={11} attached={Boolean(source.hasAvetaDocument)} />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function SlideOverServiceField({
|
||||
source,
|
||||
isCompleted,
|
||||
pmTypes,
|
||||
}: {
|
||||
source: WorkOrderTableRow;
|
||||
isCompleted: boolean;
|
||||
pmTypes: string[];
|
||||
}) {
|
||||
if (!isCompleted) return <SlideOverOpenService source={source} />;
|
||||
return (
|
||||
<PMTypeCell
|
||||
value={source.pm}
|
||||
pmTypes={pmTypes}
|
||||
serviceNotes={source.serviceNotes}
|
||||
extraServices={source.extraServices}
|
||||
avetaRequired={source.avetaRequired}
|
||||
avetaAttached={source.hasAvetaDocument}
|
||||
woNumber={source.woNumber}
|
||||
viewOnly
|
||||
onSave={() => undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SlideOverVendorField({
|
||||
source,
|
||||
isCompleted,
|
||||
vendors,
|
||||
}: {
|
||||
source: WorkOrderTableRow;
|
||||
isCompleted: boolean;
|
||||
vendors: VendorDropdownItem[];
|
||||
}) {
|
||||
if (!isCompleted) return <SlideOverInfoTabVendorField source={source} />;
|
||||
const company = effectiveCompany(source);
|
||||
return (
|
||||
<VendorCell
|
||||
vendorId={source.vendorId}
|
||||
company={company.name ?? source.company}
|
||||
tech={company.contactName ?? source.tech}
|
||||
techPhone={company.phone ?? source.techPhone}
|
||||
vendors={vendors}
|
||||
viewOnly
|
||||
frozenCompany={source.frozenCompany}
|
||||
onSave={() => undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SlideOverInfoTabDisplayView({
|
||||
source,
|
||||
isCancelled,
|
||||
|
|
@ -41,11 +186,7 @@ export function SlideOverInfoTabDisplayView({
|
|||
vendors,
|
||||
pmTypes,
|
||||
}: SlideOverInfoTabDisplayViewProps) {
|
||||
const appt = parseApptTime(source.apptTime);
|
||||
const apptDisplay = appt.start ? formatApptRange(appt.start, appt.end) : source.apptTime || "—";
|
||||
const company = effectiveCompany(source);
|
||||
const poc = effectivePoc(source);
|
||||
const primaryPoc = poc.contacts[0];
|
||||
|
||||
return (
|
||||
<div style={isCancelled ? { opacity: 0.6, pointerEvents: "none" } : undefined}>
|
||||
|
|
@ -53,38 +194,10 @@ export function SlideOverInfoTabDisplayView({
|
|||
<span className="font-mono text-[12.5px] font-semibold">{source.woNumber || "—"}</span>
|
||||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Site">
|
||||
{isCompleted ? (
|
||||
<SiteCell
|
||||
value={source.site}
|
||||
location={source.location}
|
||||
locationId={source.locationId}
|
||||
pocName={primaryPoc?.name ?? ""}
|
||||
pocPhone={primaryPoc?.phone ?? ""}
|
||||
pocNotes={poc.notes ?? ""}
|
||||
additionalContacts={poc.contacts.slice(1).map((contact) => ({
|
||||
name: contact.name ?? "",
|
||||
phone: contact.phone ?? "",
|
||||
}))}
|
||||
sites={sites}
|
||||
viewOnly
|
||||
frozenSite={source.frozenSite}
|
||||
onSave={() => undefined}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="font-mono text-[12.5px] font-bold"
|
||||
style={{ color: "var(--foreground)" }}
|
||||
>
|
||||
{source.site || "—"}
|
||||
</span>
|
||||
)}
|
||||
<SlideOverSiteField source={source} isCompleted={isCompleted} poc={poc} sites={sites} />
|
||||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Type of WO">
|
||||
{source.type ? (
|
||||
<WoTableTypeLabel type={source.type as WOType} />
|
||||
) : (
|
||||
<span style={{ color: "var(--color-text-muted)" }}>—</span>
|
||||
)}
|
||||
<SlideOverTypeField type={source.type} />
|
||||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Assigned To">
|
||||
<SlideOverInfoTabAssignedField
|
||||
|
|
@ -93,58 +206,19 @@ export function SlideOverInfoTabDisplayView({
|
|||
/>
|
||||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Schedule On">
|
||||
<span className="text-[12.5px]">
|
||||
{source.scheduleWeekOnly && source.targetWeek
|
||||
? `Week of ${source.targetWeek}`
|
||||
: source.scheduledOn || "—"}
|
||||
</span>
|
||||
<span className="text-[12.5px]">{formatScheduleOn(source)}</span>
|
||||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Due Date">
|
||||
<span className="text-[12.5px]">{source.dueDate || "—"}</span>
|
||||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Appt Time">
|
||||
<span className="text-[12.5px]">{apptDisplay}</span>
|
||||
<span className="text-[12.5px]">{formatApptDisplay(source.apptTime)}</span>
|
||||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Service">
|
||||
{isCompleted ? (
|
||||
<PMTypeCell
|
||||
value={source.pm}
|
||||
pmTypes={pmTypes}
|
||||
serviceNotes={source.serviceNotes}
|
||||
extraServices={source.extraServices}
|
||||
avetaRequired={source.avetaRequired}
|
||||
avetaAttached={source.hasAvetaDocument}
|
||||
woNumber={source.woNumber}
|
||||
viewOnly
|
||||
onSave={() => undefined}
|
||||
/>
|
||||
) : source.pm ? (
|
||||
<span className="inline-flex items-center gap-1.5 text-[12.5px]">
|
||||
<PmTypeIcon pmType={source.pm} size={14} />
|
||||
{source.pm}
|
||||
{Boolean(source.avetaRequired) && (
|
||||
<AvetaBadge size={11} attached={Boolean(source.hasAvetaDocument)} />
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ color: "var(--color-text-muted)" }}>—</span>
|
||||
)}
|
||||
<SlideOverServiceField source={source} isCompleted={isCompleted} pmTypes={pmTypes} />
|
||||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Vendor">
|
||||
{isCompleted ? (
|
||||
<VendorCell
|
||||
vendorId={source.vendorId}
|
||||
company={company.name ?? source.company}
|
||||
tech={company.contactName ?? source.tech}
|
||||
techPhone={company.phone ?? source.techPhone}
|
||||
vendors={vendors}
|
||||
viewOnly
|
||||
frozenCompany={source.frozenCompany}
|
||||
onSave={() => undefined}
|
||||
/>
|
||||
) : (
|
||||
<SlideOverInfoTabVendorField source={source} />
|
||||
)}
|
||||
<SlideOverVendorField source={source} isCompleted={isCompleted} vendors={vendors} />
|
||||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="Status">
|
||||
<SlideOverInfoTabStatusField source={source} />
|
||||
|
|
@ -154,8 +228,7 @@ export function SlideOverInfoTabDisplayView({
|
|||
</SlideOverInfoRow>
|
||||
<SlideOverInfoRow label="POC">
|
||||
<span className="text-[12.5px]" style={{ color: "var(--color-text-secondary)" }}>
|
||||
{primaryPoc?.name || "—"}
|
||||
{primaryPoc?.phone ? ` · ${primaryPoc.phone}` : ""}
|
||||
{formatPocLine(poc)}
|
||||
</span>
|
||||
</SlideOverInfoRow>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,16 @@ type VendorDialogCompanyCardProps = {
|
|||
frozenCompany?: WorkOrderFrozenCompany;
|
||||
};
|
||||
|
||||
type CompanyCardModel = {
|
||||
title: string;
|
||||
distance: string;
|
||||
contactName: string;
|
||||
email: string;
|
||||
address: string;
|
||||
mapsUrl: string;
|
||||
phone: string;
|
||||
};
|
||||
|
||||
function formatFrozenCompanyAddress(company: WorkOrderFrozenCompany): string {
|
||||
const region = [company.city, company.state]
|
||||
.map((part) => part?.trim())
|
||||
|
|
@ -19,21 +29,68 @@ function formatFrozenCompanyAddress(company: WorkOrderFrozenCompany): string {
|
|||
return [company.address?.trim(), region, company.zip?.trim()].filter(Boolean).join(", ");
|
||||
}
|
||||
|
||||
function liveMapsUrl(address: string): string {
|
||||
if (!address) return "";
|
||||
return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(address)}`;
|
||||
}
|
||||
|
||||
function resolveFrozenCompanyCard(
|
||||
company: string,
|
||||
frozen: WorkOrderFrozenCompany,
|
||||
): CompanyCardModel {
|
||||
return {
|
||||
title: company,
|
||||
distance: "",
|
||||
contactName: frozen.contactName?.trim() ?? "",
|
||||
email: frozen.email?.trim() ?? "",
|
||||
address: formatFrozenCompanyAddress(frozen),
|
||||
mapsUrl: frozen.maps ?? "",
|
||||
phone: frozen.phone ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
function resolveLiveCompanyCard(
|
||||
company: string,
|
||||
vendor: VendorDropdownItem | undefined,
|
||||
): CompanyCardModel {
|
||||
const address = vendor?.address ?? "";
|
||||
return {
|
||||
title: company,
|
||||
distance: formatVendorDistance(vendor?.distanceMiles),
|
||||
contactName: "",
|
||||
email: vendor?.email?.trim() ?? "",
|
||||
address,
|
||||
mapsUrl: liveMapsUrl(address),
|
||||
phone: "",
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCompanyCardModel(
|
||||
company: string,
|
||||
vendor: VendorDropdownItem | undefined,
|
||||
frozenCompany?: WorkOrderFrozenCompany,
|
||||
): CompanyCardModel {
|
||||
if (frozenCompany != null) return resolveFrozenCompanyCard(company, frozenCompany);
|
||||
return resolveLiveCompanyCard(company, vendor);
|
||||
}
|
||||
|
||||
function CompanyCardDetails({ model }: { model: CompanyCardModel }) {
|
||||
return (
|
||||
<>
|
||||
{Boolean(model.address) && <Detail icon={MapPin} text={model.address} />}
|
||||
{Boolean(model.contactName) && <Detail icon={User} text={model.contactName} />}
|
||||
{Boolean(model.phone) && <Detail icon={Phone} text={model.phone} />}
|
||||
{Boolean(model.email) && <Detail icon={Mail} text={model.email} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function VendorDialogCompanyCard({
|
||||
company,
|
||||
vendor,
|
||||
frozenCompany,
|
||||
}: VendorDialogCompanyCardProps) {
|
||||
const isFrozen = frozenCompany != null;
|
||||
const distance = isFrozen ? "" : formatVendorDistance(vendor?.distanceMiles);
|
||||
const contactName = isFrozen ? (frozenCompany.contactName?.trim() ?? "") : "";
|
||||
const email = isFrozen ? (frozenCompany.email?.trim() ?? "") : (vendor?.email?.trim() ?? "");
|
||||
const address = isFrozen ? formatFrozenCompanyAddress(frozenCompany) : (vendor?.address ?? "");
|
||||
const mapsUrl =
|
||||
(isFrozen ? frozenCompany.maps : undefined) ||
|
||||
(!isFrozen && address
|
||||
? `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(address)}`
|
||||
: "");
|
||||
const model = resolveCompanyCardModel(company, vendor, frozenCompany);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -45,21 +102,18 @@ export function VendorDialogCompanyCard({
|
|||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: "var(--foreground)" }}>
|
||||
{company || "—"}
|
||||
{model.title || "—"}
|
||||
</div>
|
||||
{Boolean(distance) && (
|
||||
{Boolean(model.distance) && (
|
||||
<span className="shrink-0 text-[11px]" style={{ color: "var(--muted-foreground)" }}>
|
||||
{distance}
|
||||
{model.distance}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{Boolean(address) && <Detail icon={MapPin} text={address} />}
|
||||
{Boolean(contactName) && <Detail icon={User} text={contactName} />}
|
||||
{Boolean(frozenCompany?.phone) && <Detail icon={Phone} text={frozenCompany?.phone ?? ""} />}
|
||||
{Boolean(email) && <Detail icon={Mail} text={email} />}
|
||||
{Boolean(mapsUrl) && (
|
||||
<CompanyCardDetails model={model} />
|
||||
{Boolean(model.mapsUrl) && (
|
||||
<a
|
||||
href={mapsUrl}
|
||||
href={model.mapsUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-[12px] hover:underline"
|
||||
|
|
|
|||
|
|
@ -7,37 +7,45 @@ function buildLabel(city?: string, state?: string): string {
|
|||
return [city, state].filter(Boolean).join(", ");
|
||||
}
|
||||
|
||||
function toCompDocSite(street: string, city: string, state: string, zip: string): CompDocSiteInfo {
|
||||
const label = buildLabel(city, state);
|
||||
const fullAddress = [street, label, zip].filter(Boolean).join(", ") || "—";
|
||||
return { street, city, state, zip, label, fullAddress };
|
||||
}
|
||||
|
||||
function resolveLiveLocationSite(location: Location): CompDocSiteInfo {
|
||||
return toCompDocSite(
|
||||
location.address ?? "",
|
||||
location.city ?? "",
|
||||
location.state ?? "",
|
||||
location.zipCode ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
function resolveRowLocationSite(row: Pick<WorkOrderTableRow, "location">): CompDocSiteInfo {
|
||||
const locationParts = row.location.split(",").map((part) => part.trim());
|
||||
const city = locationParts[0] ?? "";
|
||||
const state = locationParts[1] ?? "";
|
||||
const label = buildLabel(city, state);
|
||||
const street = row.location && !label ? row.location : "";
|
||||
return toCompDocSite(street, city, state, "");
|
||||
}
|
||||
|
||||
export function resolveCompDocSite(
|
||||
row: Pick<WorkOrderTableRow, "site" | "location" | "status" | "frozenSite">,
|
||||
location?: Location | null,
|
||||
): CompDocSiteInfo {
|
||||
const frozen = effectiveSite(row);
|
||||
if (frozen != null) {
|
||||
const city = frozen.city ?? "";
|
||||
const state = frozen.state ?? "";
|
||||
const zip = frozen.zip ?? "";
|
||||
const label = buildLabel(city, state);
|
||||
const street = frozen.address ?? "";
|
||||
const fullAddress = [street, label, zip].filter(Boolean).join(", ") || "—";
|
||||
return { street, city, state, zip, label, fullAddress };
|
||||
return toCompDocSite(
|
||||
frozen.address ?? "",
|
||||
frozen.city ?? "",
|
||||
frozen.state ?? "",
|
||||
frozen.zip ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
if (location?.address || location?.city) {
|
||||
const city = location.city ?? "";
|
||||
const state = location.state ?? "";
|
||||
const zip = location.zipCode ?? "";
|
||||
const label = buildLabel(city, state);
|
||||
const street = location.address ?? "";
|
||||
const fullAddress = [street, label, zip].filter(Boolean).join(", ") || "—";
|
||||
return { street, city, state, zip, label, fullAddress };
|
||||
return resolveLiveLocationSite(location);
|
||||
}
|
||||
|
||||
const locationParts = row.location.split(",").map((s) => s.trim());
|
||||
const city = locationParts[0] ?? "";
|
||||
const state = locationParts[1] ?? "";
|
||||
const label = buildLabel(city, state);
|
||||
const street = row.location && !label ? row.location : "";
|
||||
const fullAddress = [street, label].filter(Boolean).join(", ") || "—";
|
||||
|
||||
return { street, city, state, zip: "", label, fullAddress };
|
||||
return resolveRowLocationSite(row);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue