import type { InventoryItemOptionDto, PurchaseLineInput, PurchaseOrderInput, PurchaseVendorOptionDto, SalesOrderPlanningDto, SalesOrderPlanningNodeDto } from "@mrp/shared"; import { useEffect, useState } from "react"; import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom"; import { ConfirmActionDialog } from "../../components/ConfirmActionDialog"; import { useAuth } from "../../auth/AuthProvider"; import { api, ApiError } from "../../lib/api"; import { inventoryUnitOptions } from "../inventory/config"; import { emptyPurchaseOrderInput, purchaseStatusOptions } from "./config"; export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) { const { token } = useAuth(); const navigate = useNavigate(); const { orderId } = useParams(); const [searchParams] = useSearchParams(); const seededVendorId = searchParams.get("vendorId"); const planningOrderId = searchParams.get("planningOrderId"); const selectedPlanningItemId = searchParams.get("itemId"); const [form, setForm] = useState(emptyPurchaseOrderInput); const [status, setStatus] = useState(mode === "create" ? "Create a new purchase order." : "Loading purchase order..."); const [vendors, setVendors] = useState([]); const [vendorSearchTerm, setVendorSearchTerm] = useState(""); const [vendorPickerOpen, setVendorPickerOpen] = useState(false); const [itemOptions, setItemOptions] = useState([]); const [lineSearchTerms, setLineSearchTerms] = useState([]); const [activeLinePicker, setActiveLinePicker] = useState(null); const [isSaving, setIsSaving] = useState(false); const [pendingLineRemovalIndex, setPendingLineRemovalIndex] = useState(null); function collectRecommendedPurchaseNodes(node: SalesOrderPlanningNodeDto): SalesOrderPlanningNodeDto[] { const nodes = node.recommendedPurchaseQuantity > 0 ? [node] : []; for (const child of node.children) { nodes.push(...collectRecommendedPurchaseNodes(child)); } return nodes; } const subtotal = form.lines.reduce((sum: number, line: PurchaseLineInput) => sum + line.quantity * line.unitCost, 0); const taxAmount = subtotal * (form.taxPercent / 100); const total = subtotal + taxAmount + form.freightAmount; useEffect(() => { if (!token) { return; } api.getPurchaseVendors(token).then((nextVendors) => { setVendors(nextVendors); if (mode === "create" && seededVendorId) { const seededVendor = nextVendors.find((vendor) => vendor.id === seededVendorId); if (seededVendor) { setForm((current: PurchaseOrderInput) => ({ ...current, vendorId: seededVendor.id })); setVendorSearchTerm(seededVendor.name); } } }).catch(() => setVendors([])); api.getInventoryItemOptions(token).then((options) => setItemOptions(options.filter((option: InventoryItemOptionDto) => option.isPurchasable))).catch(() => setItemOptions([])); }, [mode, seededVendorId, token]); useEffect(() => { if (!token || mode !== "create" || !planningOrderId || itemOptions.length === 0) { return; } api.getSalesOrderPlanning(token, planningOrderId) .then((planning: SalesOrderPlanningDto) => { const recommendedNodes = planning.lines.flatMap((line) => collectRecommendedPurchaseNodes(line.rootNode).map((node) => ({ salesOrderLineId: node.itemId === line.itemId ? line.lineId : null, ...node, })) ); const filteredNodes = selectedPlanningItemId ? recommendedNodes.filter((node) => node.itemId === selectedPlanningItemId) : recommendedNodes; const recommendedLines = filteredNodes.map((node, index) => { const inventoryItem = itemOptions.find((option) => option.id === node.itemId); return { itemId: node.itemId, description: node.itemName, quantity: node.recommendedPurchaseQuantity, unitOfMeasure: node.unitOfMeasure, unitCost: inventoryItem?.defaultCost ?? 0, salesOrderId: planning.orderId, salesOrderLineId: node.salesOrderLineId, position: (index + 1) * 10, } satisfies PurchaseLineInput; }); if (recommendedLines.length === 0) { return; } const preferredVendorIds = [ ...new Set( recommendedLines .map((line) => itemOptions.find((option) => option.id === line.itemId)?.preferredVendorId) .filter((vendorId): vendorId is string => Boolean(vendorId)) ), ]; const autoVendorId = seededVendorId || (preferredVendorIds.length === 1 ? preferredVendorIds[0] : null); setForm((current) => ({ ...current, vendorId: current.vendorId || autoVendorId || "", notes: current.notes || `Demand-planning recommendation from sales order ${planning.documentNumber}.`, lines: current.lines.length > 0 ? current.lines : recommendedLines, })); if (autoVendorId) { const autoVendor = vendors.find((vendor) => vendor.id === autoVendorId); if (autoVendor) { setVendorSearchTerm(autoVendor.name); } } setLineSearchTerms((current) => current.length > 0 ? current : recommendedLines.map((line) => itemOptions.find((option) => option.id === line.itemId)?.sku ?? "") ); setStatus( preferredVendorIds.length > 1 && !seededVendorId ? `Loaded ${recommendedLines.length} recommended buy lines from ${planning.documentNumber}. Multiple preferred vendors exist, so confirm the vendor before saving.` : `Loaded ${recommendedLines.length} recommended buy lines from ${planning.documentNumber}.` ); }) .catch(() => { setStatus("Unable to load demand-planning recommendations."); }); }, [itemOptions, mode, planningOrderId, seededVendorId, selectedPlanningItemId, token, vendors]); useEffect(() => { if (!token || mode !== "edit" || !orderId) { return; } api.getPurchaseOrder(token, orderId) .then((document) => { setForm({ vendorId: document.vendorId, status: document.status, issueDate: document.issueDate, taxPercent: document.taxPercent, freightAmount: document.freightAmount, notes: document.notes, revisionReason: "", lines: document.lines.map((line: { itemId: string; description: string; quantity: number; unitOfMeasure: PurchaseLineInput["unitOfMeasure"]; unitCost: number; position: number; salesOrderId: string | null; salesOrderLineId: string | null }) => ({ itemId: line.itemId, description: line.description, quantity: line.quantity, unitOfMeasure: line.unitOfMeasure, unitCost: line.unitCost, salesOrderId: line.salesOrderId, salesOrderLineId: line.salesOrderLineId, position: line.position, })), }); setVendorSearchTerm(document.vendorName); setLineSearchTerms(document.lines.map((line: { itemSku: string }) => line.itemSku)); setStatus("Purchase order loaded."); }) .catch((error: unknown) => { const message = error instanceof ApiError ? error.message : "Unable to load purchase order."; setStatus(message); }); }, [mode, orderId, token]); function updateField(key: Key, value: PurchaseOrderInput[Key]) { setForm((current: PurchaseOrderInput) => ({ ...current, [key]: value })); } function getSelectedVendorName(vendorId: string) { return vendors.find((vendor) => vendor.id === vendorId)?.name ?? ""; } function getSelectedVendor(vendorId: string) { return vendors.find((vendor) => vendor.id === vendorId) ?? null; } function updateLine(index: number, nextLine: PurchaseLineInput) { setForm((current: PurchaseOrderInput) => ({ ...current, lines: current.lines.map((line: PurchaseLineInput, lineIndex: number) => (lineIndex === index ? nextLine : line)), })); } function updateLineSearchTerm(index: number, value: string) { setLineSearchTerms((current) => { const next = [...current]; next[index] = value; return next; }); } function addLine() { setForm((current: PurchaseOrderInput) => ({ ...current, lines: [ ...current.lines, { itemId: "", description: "", quantity: 1, unitOfMeasure: "EA", unitCost: 0, position: current.lines.length === 0 ? 10 : Math.max(...current.lines.map((line: PurchaseLineInput) => line.position)) + 10, }, ], })); setLineSearchTerms((current) => [...current, ""]); } function removeLine(index: number) { setForm((current: PurchaseOrderInput) => ({ ...current, lines: current.lines.filter((_line: PurchaseLineInput, lineIndex: number) => lineIndex !== index), })); setLineSearchTerms((current) => current.filter((_term, termIndex) => termIndex !== index)); } const pendingLineRemoval = pendingLineRemovalIndex != null ? { index: pendingLineRemovalIndex, line: form.lines[pendingLineRemovalIndex], sku: lineSearchTerms[pendingLineRemovalIndex] ?? "", } : null; async function handleSubmit(event: React.FormEvent) { event.preventDefault(); if (!token) { return; } setIsSaving(true); setStatus("Saving purchase order..."); try { const saved = mode === "create" ? await api.createPurchaseOrder(token, form) : await api.updatePurchaseOrder(token, orderId ?? "", form); navigate(`/purchasing/orders/${saved.id}`); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to save purchase order."; setStatus(message); setIsSaving(false); } } const filteredVendorCount = vendors.filter((vendor) => { const query = vendorSearchTerm.trim().toLowerCase(); if (!query) { return true; } return vendor.name.toLowerCase().includes(query) || vendor.email.toLowerCase().includes(query); }).length; return (

Purchasing Editor

{mode === "create" ? "New Purchase Order" : "Edit Purchase Order"}

Cancel