import type { InventoryItemOptionDto } from "@mrp/shared/dist/inventory/types.js"; import type { SalesCustomerOptionDto, SalesDocumentDetailDto, SalesDocumentInput, SalesLineInput } from "@mrp/shared/dist/sales/types.js"; import { useEffect, useState } from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { useAuth } from "../../auth/AuthProvider"; import { api, ApiError } from "../../lib/api"; import { inventoryUnitOptions } from "../inventory/config"; import { emptySalesDocumentInput, salesConfigs, salesStatusOptions, type SalesDocumentEntity } from "./config"; export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; mode: "create" | "edit" }) { const { token } = useAuth(); const navigate = useNavigate(); const { quoteId, orderId } = useParams(); const documentId = entity === "quote" ? quoteId : orderId; const config = salesConfigs[entity]; const [form, setForm] = useState(emptySalesDocumentInput); const [status, setStatus] = useState(mode === "create" ? `Create a new ${config.singularLabel.toLowerCase()}.` : `Loading ${config.singularLabel.toLowerCase()}...`); const [customers, setCustomers] = useState([]); const [customerSearchTerm, setCustomerSearchTerm] = useState(""); const [customerPickerOpen, setCustomerPickerOpen] = useState(false); const [itemOptions, setItemOptions] = useState([]); const [lineSearchTerms, setLineSearchTerms] = useState([]); const [activeLinePicker, setActiveLinePicker] = useState(null); const [isSaving, setIsSaving] = useState(false); const subtotal = form.lines.reduce((sum, line) => sum + line.quantity * line.unitPrice, 0); const discountAmount = subtotal * (form.discountPercent / 100); const taxableSubtotal = subtotal - discountAmount; const taxAmount = taxableSubtotal * (form.taxPercent / 100); const total = taxableSubtotal + taxAmount + form.freightAmount; useEffect(() => { if (!token) { return; } api.getSalesCustomers(token).then(setCustomers).catch(() => setCustomers([])); api.getInventoryItemOptions(token).then(setItemOptions).catch(() => setItemOptions([])); }, [token]); useEffect(() => { if (!token || mode !== "edit" || !documentId) { return; } const loader = entity === "quote" ? api.getQuote(token, documentId) : api.getSalesOrder(token, documentId); loader .then((document) => { setForm({ customerId: document.customerId, status: document.status, issueDate: document.issueDate, expiresAt: entity === "quote" ? document.expiresAt : null, discountPercent: document.discountPercent, taxPercent: document.taxPercent, freightAmount: document.freightAmount, notes: document.notes, lines: document.lines.map((line) => ({ itemId: line.itemId, description: line.description, quantity: line.quantity, unitOfMeasure: line.unitOfMeasure, unitPrice: line.unitPrice, position: line.position, })), }); setCustomerSearchTerm(document.customerName); setLineSearchTerms(document.lines.map((line: SalesDocumentDetailDto["lines"][number]) => line.itemSku)); setStatus(`${config.singularLabel} loaded.`); }) .catch((error: unknown) => { const message = error instanceof ApiError ? error.message : `Unable to load ${config.singularLabel.toLowerCase()}.`; setStatus(message); }); }, [config.singularLabel, documentId, entity, mode, token]); function updateField(key: Key, value: SalesDocumentInput[Key]) { setForm((current: SalesDocumentInput) => ({ ...current, [key]: value })); } function getSelectedCustomerName(customerId: string) { return customers.find((customer) => customer.id === customerId)?.name ?? ""; } function getSelectedCustomer(customerId: string) { return customers.find((customer) => customer.id === customerId) ?? null; } function updateLine(index: number, nextLine: SalesLineInput) { setForm((current: SalesDocumentInput) => ({ ...current, lines: current.lines.map((line: SalesLineInput, lineIndex: number) => (lineIndex === index ? nextLine : line)), })); } function updateLineSearchTerm(index: number, value: string) { setLineSearchTerms((current: string[]) => { const next = [...current]; next[index] = value; return next; }); } function addLine() { setForm((current: SalesDocumentInput) => ({ ...current, lines: [ ...current.lines, { itemId: "", description: "", quantity: 1, unitOfMeasure: "EA", unitPrice: 0, position: current.lines.length === 0 ? 10 : Math.max(...current.lines.map((line: SalesLineInput) => line.position)) + 10, }, ], })); setLineSearchTerms((current: string[]) => [...current, ""]); } function removeLine(index: number) { setForm((current: SalesDocumentInput) => ({ ...current, lines: current.lines.filter((_line: SalesLineInput, lineIndex: number) => lineIndex !== index), })); setLineSearchTerms((current: string[]) => current.filter((_term: string, termIndex: number) => termIndex !== index)); } async function handleSubmit(event: React.FormEvent) { event.preventDefault(); if (!token) { return; } setIsSaving(true); setStatus(`Saving ${config.singularLabel.toLowerCase()}...`); try { const saved = entity === "quote" ? mode === "create" ? await api.createQuote(token, form) : await api.updateQuote(token, documentId ?? "", form) : mode === "create" ? await api.createSalesOrder(token, { ...form, expiresAt: null }) : await api.updateSalesOrder(token, documentId ?? "", { ...form, expiresAt: null }); navigate(`${config.routeBase}/${saved.id}`); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : `Unable to save ${config.singularLabel.toLowerCase()}.`; setStatus(message); setIsSaving(false); } } return (

{config.detailEyebrow} Editor

{mode === "create" ? `New ${config.singularLabel}` : `Edit ${config.singularLabel}`}

Cancel
{entity === "quote" ? ( ) : null}