import { useEffect, useState, type RefObject } from "react"; import { Column, Grid, InlineNotification, Modal, Select, SelectItem, TextArea, TextInput } from "@carbon/react"; import { api } from "../lib/api"; import { GST_CATEGORY_LABELS, INDIAN_STATES, blankClient, type Client, type GstCategory, } from "../lib/types"; import { composeAddress, pincodeError, splitLegacyAddress } from "../lib/clientAddress"; import { clientGstinStateError } from "../lib/validators"; import { useToast } from "./ToastProvider"; interface ClientModalProps { open: boolean; /** The client to edit; null opens a blank one. */ client: Client | null; onClose: () => void; onSaved: (client: Client) => void; /** Focus returns here when the modal closes. */ launcherRef?: RefObject; } /** Fills the structured address from the legacy free-text one when a client predates the new fields. */ export function prepareForEdit(client: Client): { form: Client; splitFromLegacy: boolean } { const structured = client.addressLine1 || client.addressLine2 || client.city || client.pincode; if (structured || !client.address.trim()) return { form: client, splitFromLegacy: false }; return { form: { ...client, ...splitLegacyAddress(client.address) }, splitFromLegacy: true }; } export default function ClientModal({ open, client, onClose, onSaved, launcherRef }: ClientModalProps) { const toast = useToast(); const [form, setForm] = useState(blankClient()); const [split, setSplit] = useState(false); const [busy, setBusy] = useState(false); const [touched, setTouched] = useState(false); useEffect(() => { if (!open) return; const prepared = prepareForEdit(client ?? blankClient()); setForm(prepared.form); setSplit(prepared.splitFromLegacy); setBusy(false); setTouched(false); }, [open, client]); const set = (key: K, value: Client[K]) => setForm((prev) => ({ ...prev, [key]: value })); const nameProblem = form.name.trim() ? undefined : "Client name is required"; const gstinProblem = clientGstinStateError(form.gstin, form.stateCode); const pinProblem = pincodeError(form.pincode); const termsProblem = form.paymentTermsDays !== null && (form.paymentTermsDays < 0 || form.paymentTermsDays > 365) ? "Between 0 and 365 days" : undefined; const blocking = nameProblem ?? gstinProblem ?? pinProblem ?? termsProblem; const submit = async () => { setTouched(true); if (blocking) return; setBusy(true); const structured = form.addressLine1 || form.addressLine2 || form.city || form.pincode; const toSave: Client = { ...form, name: form.name.trim(), gstin: form.gstin.trim().toUpperCase(), address: structured ? composeAddress({ line1: form.addressLine1, line2: form.addressLine2, city: form.city, stateCode: form.stateCode, pincode: form.pincode, }) : form.address, }; try { const saved = await api.saveClient(toSave); toast.success(toSave.id === null ? "Client added" : "Client updated", saved.name); onSaved(saved); } catch (e) { toast.error("Could not save client", String(e)); setBusy(false); } }; return ( void submit()} launcherButtonRef={launcherRef} size="md" > {split ? ( ) : null} set("name", e.target.value)} invalid={touched && Boolean(nameProblem)} invalidText={nameProblem} data-modal-primary-focus /> set("gstin", e.target.value.toUpperCase())} invalid={Boolean(form.gstin.trim()) && Boolean(gstinProblem)} invalidText={gstinProblem} /> set("addressLine1", e.target.value)} /> set("addressLine2", e.target.value)} /> set("city", e.target.value)} /> set("pincode", e.target.value)} invalid={Boolean(pinProblem)} invalidText={pinProblem} /> set("poNumber", e.target.value)} /> { const raw = e.target.value.trim(); set("paymentTermsDays", raw === "" ? null : Number.isFinite(Number(raw)) ? Math.trunc(Number(raw)) : null); }} invalid={Boolean(termsProblem)} invalidText={termsProblem} />