shoc-frontend-new/src/app/(protected)/workorders/_components/dispatch-create-modal.tsx

377 lines
12 KiB
TypeScript

import { useMemo, useState } from "react";
import {
Box,
Button,
Chip,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
MenuItem,
Radio,
Stack,
TextField,
Typography,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import { useWorkOrdersList } from "@/domain/work-orders/use-cases/use-work-orders-list";
import {
useTaskListTemplates,
useWorkOrderVendors,
} from "@/domain/work-orders/use-cases/use-work-order-detail";
import { useDispatchToVendor } from "@/domain/work-orders/use-cases/use-work-order-mutations";
import type { WorkOrderDetail } from "@/domain/work-orders/types/work-order";
interface SelectedWorkOrder {
id: string | number;
number: string;
title: string;
}
interface DispatchCreateModalProps {
workOrder: WorkOrderDetail;
onClose: () => void;
onDispatched: () => void;
}
export function DispatchCreateModal({
workOrder,
onClose,
onDispatched,
}: DispatchCreateModalProps) {
const [selectedVendorId, setSelectedVendorId] = useState<string | number | null>(null);
const [nteAmount, setNteAmount] = useState(workOrder.vendorNTE || "");
const [scheduledDate, setScheduledDate] = useState("");
const [description, setDescription] = useState(workOrder.description || "");
const [vendorSearch, setVendorSearch] = useState("");
const [selectedTemplateId, setSelectedTemplateId] = useState("");
const [customItems, setCustomItems] = useState<string[]>([]);
const [newCustomItem, setNewCustomItem] = useState("");
const [selectedWOs, setSelectedWOs] = useState<SelectedWorkOrder[]>([
{
id: workOrder.id,
number: workOrder.internalWONumber,
title: workOrder.title,
},
]);
const [woSearch, setWoSearch] = useState("");
const { data: vendors = [], isLoading: isLoadingVendors } = useWorkOrderVendors(
workOrder.trade,
workOrder.locationZip,
);
const { data: taskTemplates = [] } = useTaskListTemplates();
const dispatchMutation = useDispatchToVendor(workOrder.id);
const { data: siteWorkOrdersData } = useWorkOrdersList({
search: "",
page: 1,
pageSize: 500,
sortBy: "number",
sortDir: "asc",
locationId: workOrder.locationId,
});
const siteWorkOrders = useMemo(
() =>
(siteWorkOrdersData?.items ?? []).filter((item) => String(item.id) !== String(workOrder.id)),
[siteWorkOrdersData?.items, workOrder.id],
);
const filteredVendors = useMemo(
() =>
vendors.filter(
(vendor) =>
vendor.companyName.toLowerCase().includes(vendorSearch.toLowerCase()) ||
vendor.tradeSpecialties.toLowerCase().includes(vendorSearch.toLowerCase()),
),
[vendors, vendorSearch],
);
const filteredSiteWOs = useMemo(
() =>
siteWorkOrders
.filter((item) => !selectedWOs.some((selected) => String(selected.id) === String(item.id)))
.filter(
(item) =>
!woSearch ||
item.woNumber.includes(woSearch) ||
item.title.toLowerCase().includes(woSearch.toLowerCase()),
)
.slice(0, 10),
[siteWorkOrders, selectedWOs, woSearch],
);
const handleSubmit = () => {
if (!selectedVendorId) {
return;
}
dispatchMutation.mutate(
{
workOrderIds: selectedWOs.map((wo) => wo.id),
vendorIds: [selectedVendorId],
nteAmount: nteAmount ? parseFloat(nteAmount) : null,
scheduledDate: scheduledDate || null,
description,
taskListTemplateId: selectedTemplateId ? parseInt(selectedTemplateId, 10) : null,
customChecklistItems: customItems.length > 0 ? customItems : null,
},
{
onSuccess: () => {
onDispatched();
onClose();
},
},
);
};
return (
<Dialog open onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle className="flex items-center justify-between pr-2">
Dispatch{" "}
{selectedWOs.length > 1
? `${selectedWOs.length} Work Orders`
: `Work Order ${workOrder.internalWONumber}`}
<IconButton aria-label="Close" onClick={onClose} size="small">
<CloseIcon fontSize="small" />
</IconButton>
</DialogTitle>
<DialogContent className="flex flex-col gap-4">
<Box>
<Typography variant="subtitle2" className="mb-2">
Work Orders ({selectedWOs.length})
</Typography>
<Stack
direction="row"
sx={{
flexWrap: "wrap",
gap: 1,
}}
>
{selectedWOs.map((wo) => (
<Chip
key={String(wo.id)}
label={`${wo.number} — ${wo.title}`}
onDelete={
String(wo.id) !== String(workOrder.id)
? () => setSelectedWOs((prev) => prev.filter((item) => item.id !== wo.id))
: undefined
}
/>
))}
</Stack>
{siteWorkOrders.length > 0 && (
<Box className="mt-3">
<TextField
size="small"
value={woSearch}
onChange={(event) => setWoSearch(event.target.value)}
placeholder="Search to filter..."
fullWidth
className="mb-2"
/>
<PaperList>
{filteredSiteWOs.map((item) => (
<Box
key={String(item.id)}
className="cursor-pointer rounded px-2 py-1 hover:bg-muted/50"
onClick={() => {
setSelectedWOs((prev) => [
...prev,
{ id: item.id, number: item.woNumber, title: item.title },
]);
setWoSearch("");
}}
>
<Typography variant="body2">
<strong>{item.woNumber}</strong> — {item.title}
</Typography>
</Box>
))}
</PaperList>
</Box>
)}
</Box>
<Box>
<Typography variant="subtitle2" className="mb-2">
Select Vendor
</Typography>
<TextField
size="small"
value={vendorSearch}
onChange={(event) => setVendorSearch(event.target.value)}
placeholder="Search vendors..."
fullWidth
className="mb-2"
/>
{isLoadingVendors ? (
<Typography
sx={{
color: "text.secondary",
}}
>
Loading vendors...
</Typography>
) : filteredVendors.length === 0 ? (
<Typography
sx={{
color: "text.secondary",
}}
>
No vendors found
</Typography>
) : (
<Stack spacing={1} className="max-h-60 overflow-y-auto">
{filteredVendors.map((vendor) => (
<Box
key={String(vendor.id)}
className={`flex cursor-pointer items-start gap-2 rounded border p-2 ${
selectedVendorId === vendor.id
? "border-primary bg-primary/10"
: "border-border"
}`}
onClick={() =>
setSelectedVendorId((current) => (current === vendor.id ? null : vendor.id))
}
>
<Radio checked={selectedVendorId === vendor.id} readOnly size="small" />
<Box className="flex-1">
<Typography variant="body2" className="font-semibold">
{vendor.companyName}
</Typography>
{vendor.tradeSpecialties && (
<Typography
variant="caption"
sx={{
color: "text.secondary",
display: "block",
}}
>
{vendor.tradeSpecialties}
</Typography>
)}
{vendor.address && (
<Typography
variant="caption"
sx={{
color: "text.secondary",
display: "block",
}}
>
{vendor.address}
</Typography>
)}
</Box>
{vendor.distanceMiles != null && (
<Typography variant="caption" className="whitespace-nowrap">
{vendor.distanceMiles} miles
</Typography>
)}
</Box>
))}
</Stack>
)}
</Box>
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
<TextField
label="NTE Amount"
type="number"
value={nteAmount}
onChange={(event) => setNteAmount(event.target.value)}
fullWidth
slotProps={{
htmlInput: { step: "0.01", min: 0 },
}}
/>
<TextField
label="Vendor Scheduled Date"
type="date"
value={scheduledDate}
onChange={(event) => setScheduledDate(event.target.value)}
slotProps={{ inputLabel: { shrink: true } }}
fullWidth
/>
</Stack>
<TextField
label="Task List Template"
select
value={selectedTemplateId}
onChange={(event) => setSelectedTemplateId(event.target.value)}
fullWidth
>
<MenuItem value="">No checklist template</MenuItem>
{taskTemplates.map((template) => (
<MenuItem key={String(template.id)} value={String(template.id)}>
{template.name} ({template.items?.length ?? 0} items)
</MenuItem>
))}
</TextField>
{customItems.length > 0 && (
<Stack
direction="row"
sx={{
flexWrap: "wrap",
gap: 1,
}}
>
{customItems.map((item, index) => (
<Chip
key={`${item}-${index}`}
label={item}
onDelete={() => setCustomItems((prev) => prev.filter((_, i) => i !== index))}
/>
))}
</Stack>
)}
<TextField
value={newCustomItem}
onChange={(event) => setNewCustomItem(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter" && newCustomItem.trim()) {
event.preventDefault();
setCustomItems((prev) => [...prev, newCustomItem.trim()]);
setNewCustomItem("");
}
}}
placeholder="Add custom checklist item..."
fullWidth
size="small"
/>
<TextField
label="Description"
multiline
rows={4}
value={description}
onChange={(event) => setDescription(event.target.value)}
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={dispatchMutation.isPending}>
Cancel
</Button>
<Button
variant="contained"
onClick={handleSubmit}
disabled={dispatchMutation.isPending || !selectedVendorId}
>
{dispatchMutation.isPending ? "Dispatching..." : "Dispatch"}
</Button>
</DialogActions>
</Dialog>
);
}
function PaperList({ children }: { children: React.ReactNode }) {
return (
<Box className="max-h-40 overflow-y-auto rounded border border-border p-1">{children}</Box>
);
}