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 { useEffect, useMemo, useState, type MouseEvent } from "react";
|
||||||
import { Wrench } from "lucide-react";
|
|
||||||
import { Dialog, DialogContent, DialogTitle } from "@mui/material";
|
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 { Text } from "@/components/ui/text";
|
||||||
import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop";
|
import { dialogBlurBackdropSlotProps } from "@/components/ui/dialog-blur-backdrop";
|
||||||
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
|
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
|
||||||
import type { VendorFormValues } from "@/domain/vendors/schemas/vendor-schema";
|
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 { useCreateVendor } from "@/domain/vendors/use-cases/use-create-vendor";
|
||||||
import { FieldLabel } from "@/app/(protected)/workorders/_components/list/table/cells/dialog-field-label";
|
|
||||||
import {
|
import {
|
||||||
buildTechnicianOptions,
|
buildTechnicianOptions,
|
||||||
phoneForOption,
|
phoneForOption,
|
||||||
} from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-options";
|
} 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 { VendorDialogBody } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-body";
|
||||||
import { VendorDialogAddTechnician } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-add-technician";
|
import { VendorDialogFooter } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-footer";
|
||||||
import {
|
import {
|
||||||
mergeVendorDropdownItems,
|
mergeVendorDropdownItems,
|
||||||
resolveVendorCompanyAnchor,
|
resolveVendorCompanyAnchor,
|
||||||
} from "@/app/(protected)/workorders/_components/list/table/cells/resolve-vendor-company-anchor";
|
} 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 = {
|
export type { VendorPatch } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-types";
|
||||||
vendorId: string;
|
|
||||||
company: string;
|
|
||||||
tech: string;
|
|
||||||
techPhone: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type VendorDialogProps = {
|
type VendorDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -114,38 +101,20 @@ export function VendorDialog({
|
||||||
setPhone(createdPhone);
|
setPhone(createdPhone);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveSaveVendor = () =>
|
const { handleSave } = useVendorDialogSave({
|
||||||
allVendors.find(
|
allVendors,
|
||||||
(vendor) =>
|
pendingCreates,
|
||||||
vendor.contactName === t && vendor.companyName === (resolvedCompany || anchorCompanyName),
|
selectedKey,
|
||||||
) ??
|
selectedVendor,
|
||||||
allVendors.find((vendor) => String(vendor.id) === selectedKey) ??
|
resolvedCompany,
|
||||||
selectedVendor;
|
anchorCompanyName,
|
||||||
|
tech: t,
|
||||||
const handleSave = async () => {
|
phone,
|
||||||
setSaveError(null);
|
createVendor,
|
||||||
let saveVendor = resolveSaveVendor();
|
onSave,
|
||||||
const pendingPayload = saveVendor ? pendingCreates[String(saveVendor.id)] : undefined;
|
onOpenChange,
|
||||||
|
setSaveError,
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|
@ -169,93 +138,28 @@ export function VendorDialog({
|
||||||
Search and select the technician
|
Search and select the technician
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<div className="mt-3 space-y-3">
|
<VendorDialogBody
|
||||||
<div>
|
selectedKey={selectedKey}
|
||||||
<FieldLabel>Technician</FieldLabel>
|
technicianOptions={technicianOptions}
|
||||||
<WizardFieldSelect
|
anchorCompanyName={anchorCompanyName}
|
||||||
value={selectedKey}
|
companyVendorForAdd={companyVendor}
|
||||||
placeholder="Search technician …"
|
tech={t}
|
||||||
icon={
|
phone={phone}
|
||||||
<Wrench
|
notes={notes}
|
||||||
size={14}
|
resolvedCompany={resolvedCompany}
|
||||||
className="shrink-0"
|
selectedVendor={selectedVendor}
|
||||||
style={{ color: "var(--color-text-muted)" }}
|
saveError={saveError}
|
||||||
/>
|
onPick={handlePick}
|
||||||
}
|
onPhoneChange={setPhone}
|
||||||
searchPlaceholder="Search technician…"
|
onNotesChange={setNotes}
|
||||||
options={technicianOptions.map((option) => ({
|
onTechnicianCreated={handleTechnicianCreated}
|
||||||
key: option.key,
|
/>
|
||||||
label: option.label,
|
|
||||||
}))}
|
|
||||||
onPick={handlePick}
|
|
||||||
/>
|
|
||||||
{companyVendor != null && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<VendorDialogAddTechnician
|
|
||||||
companyName={anchorCompanyName}
|
|
||||||
companyVendor={companyVendor}
|
|
||||||
onCreated={handleTechnicianCreated}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{Boolean(t) && (
|
<VendorDialogFooter
|
||||||
<div>
|
isPending={createVendor.isPending}
|
||||||
<FieldLabel>Technician Phone</FieldLabel>
|
onCancel={() => onOpenChange(false)}
|
||||||
<input
|
onSave={handleSave}
|
||||||
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>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue