mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
refactor(workorders): extract vendor dialog body footer and save hook
This commit is contained in:
parent
4cce4c1d69
commit
4fe4ce8465
5 changed files with 268 additions and 136 deletions
|
|
@ -0,0 +1,72 @@
|
|||
import { isDraftVendorId } from "@/domain/vendors/utils/draft-vendor";
|
||||
import { vendorToDropdownItem } from "@/domain/vendors/utils/vendor-to-dropdown-item";
|
||||
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
|
||||
import type { VendorFormValues } from "@/domain/vendors/schemas/vendor-schema";
|
||||
import type { VendorPatch } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-types";
|
||||
import type { useCreateVendor } from "@/domain/vendors/use-cases/use-create-vendor";
|
||||
|
||||
type UseVendorDialogSaveArgs = {
|
||||
allVendors: VendorDropdownItem[];
|
||||
pendingCreates: Record<string, VendorFormValues>;
|
||||
selectedKey: string;
|
||||
selectedVendor: VendorDropdownItem | undefined;
|
||||
resolvedCompany: string;
|
||||
anchorCompanyName: string;
|
||||
tech: string;
|
||||
phone: string;
|
||||
createVendor: ReturnType<typeof useCreateVendor>;
|
||||
onSave: (patch: VendorPatch) => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
setSaveError: (error: string | null) => void;
|
||||
};
|
||||
|
||||
export function useVendorDialogSave({
|
||||
allVendors,
|
||||
pendingCreates,
|
||||
selectedKey,
|
||||
selectedVendor,
|
||||
resolvedCompany,
|
||||
anchorCompanyName,
|
||||
tech,
|
||||
phone,
|
||||
createVendor,
|
||||
onSave,
|
||||
onOpenChange,
|
||||
setSaveError,
|
||||
}: UseVendorDialogSaveArgs) {
|
||||
const resolveSaveVendor = () =>
|
||||
allVendors.find(
|
||||
(vendor) =>
|
||||
vendor.contactName === tech &&
|
||||
vendor.companyName === (resolvedCompany || anchorCompanyName),
|
||||
) ??
|
||||
allVendors.find((vendor) => String(vendor.id) === selectedKey) ??
|
||||
selectedVendor;
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaveError(null);
|
||||
let saveVendor = resolveSaveVendor();
|
||||
const pendingPayload = saveVendor ? pendingCreates[String(saveVendor.id)] : undefined;
|
||||
|
||||
if (saveVendor != null && isDraftVendorId(saveVendor.id) && pendingPayload) {
|
||||
try {
|
||||
const created = await createVendor.mutateAsync(pendingPayload);
|
||||
saveVendor = vendorToDropdownItem(created);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to create technician";
|
||||
setSaveError(message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onSave({
|
||||
vendorId: saveVendor ? String(saveVendor.id) : "",
|
||||
company: resolvedCompany || anchorCompanyName,
|
||||
tech,
|
||||
techPhone: phone,
|
||||
});
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return { handleSave };
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
import { Wrench } from "lucide-react";
|
||||
import {
|
||||
DLG_INPUT_CLS,
|
||||
DLG_INPUT_STYLE,
|
||||
} from "@/app/(protected)/workorders/_components/wizard/wizard-input-styles";
|
||||
import { WizardFieldSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-field-select";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
|
||||
import { FieldLabel } from "@/app/(protected)/workorders/_components/list/table/cells/dialog-field-label";
|
||||
import { VendorDialogAddTechnician } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-add-technician";
|
||||
import { VendorDialogCompanyCard } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-company-card";
|
||||
import type { VendorFormValues } from "@/domain/vendors/schemas/vendor-schema";
|
||||
import type { TechnicianOption } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-options";
|
||||
|
||||
type VendorDialogBodyProps = {
|
||||
selectedKey: string;
|
||||
technicianOptions: TechnicianOption[];
|
||||
anchorCompanyName: string;
|
||||
companyVendorForAdd: VendorDropdownItem | undefined;
|
||||
tech: string;
|
||||
phone: string;
|
||||
notes: string;
|
||||
resolvedCompany: string;
|
||||
selectedVendor: VendorDropdownItem | undefined;
|
||||
saveError: string | null;
|
||||
onPick: (key: string) => void;
|
||||
onPhoneChange: (phone: string) => void;
|
||||
onNotesChange: (notes: string) => void;
|
||||
onTechnicianCreated: (
|
||||
created: VendorDropdownItem,
|
||||
createdPhone: string,
|
||||
payload: VendorFormValues,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export function VendorDialogBody({
|
||||
selectedKey,
|
||||
technicianOptions,
|
||||
anchorCompanyName,
|
||||
companyVendorForAdd,
|
||||
tech,
|
||||
phone,
|
||||
notes,
|
||||
resolvedCompany,
|
||||
selectedVendor,
|
||||
saveError,
|
||||
onPick,
|
||||
onPhoneChange,
|
||||
onNotesChange,
|
||||
onTechnicianCreated,
|
||||
}: VendorDialogBodyProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="mt-3 space-y-3">
|
||||
<div>
|
||||
<FieldLabel>Technician</FieldLabel>
|
||||
<WizardFieldSelect
|
||||
value={selectedKey}
|
||||
placeholder="Search technician …"
|
||||
icon={
|
||||
<Wrench size={14} className="shrink-0" style={{ color: "var(--color-text-muted)" }} />
|
||||
}
|
||||
searchPlaceholder="Search technician…"
|
||||
options={technicianOptions.map((option) => ({
|
||||
key: option.key,
|
||||
label: option.label,
|
||||
}))}
|
||||
onPick={onPick}
|
||||
/>
|
||||
{companyVendorForAdd != null && (
|
||||
<div className="mt-2">
|
||||
<VendorDialogAddTechnician
|
||||
companyName={anchorCompanyName}
|
||||
companyVendor={companyVendorForAdd}
|
||||
onCreated={onTechnicianCreated}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{Boolean(tech) && (
|
||||
<div>
|
||||
<FieldLabel>Technician Phone</FieldLabel>
|
||||
<input
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => onPhoneChange(e.target.value)}
|
||||
className={DLG_INPUT_CLS}
|
||||
style={DLG_INPUT_STYLE}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Boolean(resolvedCompany) && (
|
||||
<div>
|
||||
<FieldLabel>Company</FieldLabel>
|
||||
<VendorDialogCompanyCard company={resolvedCompany} vendor={selectedVendor} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<FieldLabel>Notes</FieldLabel>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={(e) => onNotesChange(e.target.value)}
|
||||
placeholder="Contact preferences, availability, etc."
|
||||
rows={3}
|
||||
className={`${DLG_INPUT_CLS} resize-none`}
|
||||
style={{ ...DLG_INPUT_STYLE, fontFamily: "inherit" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Boolean(saveError) && (
|
||||
<Text as="p" variant="caption" sx={{ fontSize: 11.5, color: "var(--color-destructive)" }}>
|
||||
{saveError}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type VendorDialogFooterProps = {
|
||||
isPending: boolean;
|
||||
onCancel: () => void;
|
||||
onSave: () => void;
|
||||
};
|
||||
|
||||
export function VendorDialogFooter({ isPending, onCancel, onSave }: VendorDialogFooterProps) {
|
||||
return (
|
||||
<div className="mt-4 flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="min-h-0 px-2 py-1 text-[12px]"
|
||||
disabled={isPending}
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="min-h-0 px-3 py-1 text-[12px]"
|
||||
disabled={isPending}
|
||||
onClick={() => void onSave()}
|
||||
>
|
||||
{isPending ? "Saving…" : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export type VendorPatch = {
|
||||
vendorId: string;
|
||||
company: string;
|
||||
tech: string;
|
||||
techPhone: string;
|
||||
};
|
||||
|
|
@ -1,37 +1,24 @@
|
|||
import { useEffect, useMemo, useState, type MouseEvent } from "react";
|
||||
import { Wrench } from "lucide-react";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@mui/material";
|
||||
import { WizardFieldSelect } from "@/app/(protected)/workorders/_components/wizard/wizard-field-select";
|
||||
import {
|
||||
DLG_INPUT_CLS,
|
||||
DLG_INPUT_STYLE,
|
||||
} from "@/app/(protected)/workorders/_components/wizard/wizard-input-styles";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop";
|
||||
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
|
||||
import type { VendorFormValues } from "@/domain/vendors/schemas/vendor-schema";
|
||||
import { isDraftVendorId } from "@/domain/vendors/utils/draft-vendor";
|
||||
import { vendorToDropdownItem } from "@/domain/vendors/utils/vendor-to-dropdown-item";
|
||||
import { useCreateVendor } from "@/domain/vendors/use-cases/use-create-vendor";
|
||||
import { FieldLabel } from "@/app/(protected)/workorders/_components/list/table/cells/dialog-field-label";
|
||||
import {
|
||||
buildTechnicianOptions,
|
||||
phoneForOption,
|
||||
} from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-options";
|
||||
import { VendorDialogCompanyCard } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-company-card";
|
||||
import { VendorDialogAddTechnician } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-add-technician";
|
||||
import { VendorDialogBody } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-body";
|
||||
import { VendorDialogFooter } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-footer";
|
||||
import {
|
||||
mergeVendorDropdownItems,
|
||||
resolveVendorCompanyAnchor,
|
||||
} from "@/app/(protected)/workorders/_components/list/table/cells/resolve-vendor-company-anchor";
|
||||
import { useVendorDialogSave } from "@/app/(protected)/workorders/_components/list/table/cells/use-vendor-dialog-save";
|
||||
import type { VendorPatch } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-types";
|
||||
|
||||
export type VendorPatch = {
|
||||
vendorId: string;
|
||||
company: string;
|
||||
tech: string;
|
||||
techPhone: string;
|
||||
};
|
||||
export type { VendorPatch } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-types";
|
||||
|
||||
type VendorDialogProps = {
|
||||
open: boolean;
|
||||
|
|
@ -114,38 +101,20 @@ export function VendorDialog({
|
|||
setPhone(createdPhone);
|
||||
};
|
||||
|
||||
const resolveSaveVendor = () =>
|
||||
allVendors.find(
|
||||
(vendor) =>
|
||||
vendor.contactName === t && vendor.companyName === (resolvedCompany || anchorCompanyName),
|
||||
) ??
|
||||
allVendors.find((vendor) => String(vendor.id) === selectedKey) ??
|
||||
selectedVendor;
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaveError(null);
|
||||
let saveVendor = resolveSaveVendor();
|
||||
const pendingPayload = saveVendor ? pendingCreates[String(saveVendor.id)] : undefined;
|
||||
|
||||
if (saveVendor != null && isDraftVendorId(saveVendor.id) && pendingPayload) {
|
||||
try {
|
||||
const created = await createVendor.mutateAsync(pendingPayload);
|
||||
saveVendor = vendorToDropdownItem(created);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to create technician";
|
||||
setSaveError(message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onSave({
|
||||
vendorId: saveVendor ? String(saveVendor.id) : "",
|
||||
company: resolvedCompany || anchorCompanyName,
|
||||
tech: t,
|
||||
techPhone: phone,
|
||||
});
|
||||
onOpenChange(false);
|
||||
};
|
||||
const { handleSave } = useVendorDialogSave({
|
||||
allVendors,
|
||||
pendingCreates,
|
||||
selectedKey,
|
||||
selectedVendor,
|
||||
resolvedCompany,
|
||||
anchorCompanyName,
|
||||
tech: t,
|
||||
phone,
|
||||
createVendor,
|
||||
onSave,
|
||||
onOpenChange,
|
||||
setSaveError,
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
|
@ -169,93 +138,28 @@ export function VendorDialog({
|
|||
Search and select the technician
|
||||
</Text>
|
||||
|
||||
<div className="mt-3 space-y-3">
|
||||
<div>
|
||||
<FieldLabel>Technician</FieldLabel>
|
||||
<WizardFieldSelect
|
||||
value={selectedKey}
|
||||
placeholder="Search technician …"
|
||||
icon={
|
||||
<Wrench
|
||||
size={14}
|
||||
className="shrink-0"
|
||||
style={{ color: "var(--color-text-muted)" }}
|
||||
/>
|
||||
}
|
||||
searchPlaceholder="Search technician…"
|
||||
options={technicianOptions.map((option) => ({
|
||||
key: option.key,
|
||||
label: option.label,
|
||||
}))}
|
||||
onPick={handlePick}
|
||||
/>
|
||||
{companyVendor != null && (
|
||||
<div className="mt-2">
|
||||
<VendorDialogAddTechnician
|
||||
companyName={anchorCompanyName}
|
||||
companyVendor={companyVendor}
|
||||
onCreated={handleTechnicianCreated}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<VendorDialogBody
|
||||
selectedKey={selectedKey}
|
||||
technicianOptions={technicianOptions}
|
||||
anchorCompanyName={anchorCompanyName}
|
||||
companyVendorForAdd={companyVendor}
|
||||
tech={t}
|
||||
phone={phone}
|
||||
notes={notes}
|
||||
resolvedCompany={resolvedCompany}
|
||||
selectedVendor={selectedVendor}
|
||||
saveError={saveError}
|
||||
onPick={handlePick}
|
||||
onPhoneChange={setPhone}
|
||||
onNotesChange={setNotes}
|
||||
onTechnicianCreated={handleTechnicianCreated}
|
||||
/>
|
||||
|
||||
{Boolean(t) && (
|
||||
<div>
|
||||
<FieldLabel>Technician Phone</FieldLabel>
|
||||
<input
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
className={DLG_INPUT_CLS}
|
||||
style={DLG_INPUT_STYLE}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Boolean(resolvedCompany) && (
|
||||
<div>
|
||||
<FieldLabel>Company</FieldLabel>
|
||||
<VendorDialogCompanyCard company={resolvedCompany} vendor={selectedVendor} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<FieldLabel>Notes</FieldLabel>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder="Contact preferences, availability, etc."
|
||||
rows={3}
|
||||
className={`${DLG_INPUT_CLS} resize-none`}
|
||||
style={{ ...DLG_INPUT_STYLE, fontFamily: "inherit" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Boolean(saveError) && (
|
||||
<Text as="p" variant="caption" sx={{ fontSize: 11.5, color: "var(--color-destructive)" }}>
|
||||
{saveError}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="min-h-0 px-2 py-1 text-[12px]"
|
||||
disabled={createVendor.isPending}
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="min-h-0 px-3 py-1 text-[12px]"
|
||||
disabled={createVendor.isPending}
|
||||
onClick={() => void handleSave()}
|
||||
>
|
||||
{createVendor.isPending ? "Saving…" : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
<VendorDialogFooter
|
||||
isPending={createVendor.isPending}
|
||||
onCancel={() => onOpenChange(false)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue