import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Badge, Button, Card, Container, Group, Loader, Modal, NumberInput, Select, Stack, Switch, Table, Tabs, Text, TextInput, Title, } from "@mantine/core"; import { Plus } from "lucide-react"; import Breadcrumbs from "@/components/ui/Breadcrumbs"; import { useToast } from "@/hooks/use-toast"; import { vehiclesService, type Vehicle } from "@/services/vehicles.service"; import { procurementService, type AssetAcquisition, type AssetDisposal, type Vendor, type AcquisitionType, type AcquisitionStatus, type VendorType, type DisposalMethod, } from "@/services/procurement.service"; const money = (x: number | null | undefined) => `ETB ${(Number(x) || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2, })}`; const ACQUISITION_TYPES: AcquisitionType[] = ["PURCHASE", "LEASE", "RENTAL"]; const ACQUISITION_STATUSES: AcquisitionStatus[] = ["ACTIVE", "LEASE_EXPIRING", "DISPOSED"]; const VENDOR_TYPES: VendorType[] = ["DEALER", "LEASING", "PARTS", "SERVICE", "OTHER"]; const DISPOSAL_METHODS: DisposalMethod[] = ["SALE", "SCRAP", "RETURN_LEASE", "TRADE_IN"]; const typeBadgeColor = (t: AcquisitionType) => t === "PURCHASE" ? "green" : t === "LEASE" ? "blue" : "grape"; const statusBadgeColor = (s: AcquisitionStatus) => s === "ACTIVE" ? "green" : s === "LEASE_EXPIRING" ? "yellow" : "gray"; const vehicleLabel = ( v?: { plateNumber?: string | null; registrationNumber?: string | null } | null, fallback?: string | null, ) => v?.plateNumber || v?.registrationNumber || fallback || "—"; // Strip empty strings / null / undefined before sending to the API (ValidationPipe rejects "" for UUID fields). const clean = >(obj: T): Partial => Object.fromEntries( Object.entries(obj).filter(([, v]) => v !== "" && v !== undefined && v !== null), ) as Partial; const emptyAcquisition = { itemName: "", vehicleId: "", vendorId: "", acquisitionType: "PURCHASE" as AcquisitionType, acquisitionDate: new Date().toISOString().split("T")[0], cost: undefined as number | undefined, usefulLifeMonths: undefined as number | undefined, salvageValue: undefined as number | undefined, leaseStart: "", leaseEnd: "", monthlyPayment: undefined as number | undefined, status: "ACTIVE" as AcquisitionStatus, notes: "", }; const emptyVendor = { name: "", type: "" as VendorType | "", contactPerson: "", phone: "", email: "", address: "", isActive: true, }; const emptyDisposal = { vehicleId: "", disposalDate: new Date().toISOString().split("T")[0], method: "SALE" as DisposalMethod, salePrice: undefined as number | undefined, buyer: "", notes: "", }; export default function ProcurementPage() { const { toast } = useToast(); const qc = useQueryClient(); const [tab, setTab] = useState("acquisitions"); const [acqModalOpen, setAcqModalOpen] = useState(false); const [vendorModalOpen, setVendorModalOpen] = useState(false); const [disposalModalOpen, setDisposalModalOpen] = useState(false); const [acqForm, setAcqForm] = useState({ ...emptyAcquisition }); const [vendorForm, setVendorForm] = useState({ ...emptyVendor }); const [disposalForm, setDisposalForm] = useState({ ...emptyDisposal }); // ---- Queries ---- const { data: vehiclesData } = useQuery({ queryKey: ["vehicles", "list"], queryFn: async () => { const res = await vehiclesService.getAll({ limit: 1000 }); return res.data || []; }, }); const { data: acquisitions = [], isLoading: loadingAcquisitions } = useQuery({ queryKey: ["procurement", "acquisitions"], queryFn: async () => { const res = await procurementService.listAcquisitions(); return res.data || []; }, }); const { data: vendors = [], isLoading: loadingVendors } = useQuery({ queryKey: ["procurement", "vendors"], queryFn: async () => { const res = await procurementService.listVendors(); return res.data || []; }, }); const { data: disposals = [], isLoading: loadingDisposals } = useQuery({ queryKey: ["procurement", "disposals"], queryFn: async () => { const res = await procurementService.listDisposals(); return res.data || []; }, }); const vehicleOptions = vehiclesData?.map((v: Vehicle) => ({ value: v.id, label: `${v.plateNumber} - ${v.manufacturer} ${v.model}`, })) || []; const vendorOptions = vendors.map((v: Vendor) => ({ value: v.id, label: v.name })); // ---- Mutations ---- const createAcquisition = useMutation({ mutationFn: async () => { const res = await procurementService.createAcquisition(clean(acqForm) as never); return res.data; }, onSuccess: () => { toast({ title: "Acquisition recorded" }); setAcqModalOpen(false); setAcqForm({ ...emptyAcquisition }); qc.invalidateQueries({ queryKey: ["procurement", "acquisitions"] }); }, onError: (error: any) => { toast({ title: "Error recording acquisition", description: error?.response?.data?.message || "Failed to record acquisition", variant: "destructive", }); }, }); const createVendor = useMutation({ mutationFn: async () => { const res = await procurementService.createVendor(clean(vendorForm) as never); return res.data; }, onSuccess: () => { toast({ title: "Vendor created" }); setVendorModalOpen(false); setVendorForm({ ...emptyVendor }); qc.invalidateQueries({ queryKey: ["procurement", "vendors"] }); }, onError: (error: any) => { toast({ title: "Error creating vendor", description: error?.response?.data?.message || "Failed to create vendor", variant: "destructive", }); }, }); const createDisposal = useMutation({ mutationFn: async () => { const res = await procurementService.createDisposal(clean(disposalForm) as never); return res.data; }, onSuccess: () => { toast({ title: "Disposal recorded" }); setDisposalModalOpen(false); setDisposalForm({ ...emptyDisposal }); qc.invalidateQueries({ queryKey: ["procurement", "disposals"] }); }, onError: (error: any) => { toast({ title: "Error recording disposal", description: error?.response?.data?.message || "Failed to record disposal", variant: "destructive", }); }, }); return ( Procurement & Assets setTab(val || "acquisitions")}> Acquisitions Vendors Disposals {/* ---- Acquisitions ---- */} Item / Asset Vehicle Type Date Cost Status {loadingAcquisitions ? ( ) : acquisitions.length === 0 ? ( No acquisitions recorded yet. ) : null} {acquisitions.map((a: AssetAcquisition) => ( {a.itemName || "—"} {vehicleLabel(a.vehicle, a.vehicleId)} {a.acquisitionType} {new Date(a.acquisitionDate).toLocaleDateString()} {money(a.cost)} {a.status} ))}
{/* ---- Vendors ---- */} Name Type Contact Phone Email Active {loadingVendors ? ( ) : vendors.length === 0 ? ( No vendors added yet. ) : null} {vendors.map((v: Vendor) => ( {v.name} {v.type ? {v.type} : "—"} {v.contactPerson || "—"} {v.phone || "—"} {v.email || "—"} {v.isActive ? "Active" : "Inactive"} ))}
{/* ---- Disposals ---- */} Vehicle Method Date Sale Price Buyer {loadingDisposals ? ( ) : disposals.length === 0 ? ( No disposals recorded yet. ) : null} {disposals.map((d: AssetDisposal) => ( {d.vehicleId} {d.method} {new Date(d.disposalDate).toLocaleDateString()} {money(d.salePrice)} {d.buyer || "—"} ))}
{/* ---- Acquisition Modal ---- */} setAcqModalOpen(false)} title="New Acquisition" size="lg" > setAcqForm({ ...acqForm, itemName: e.currentTarget.value })} required /> setAcqForm({ ...acqForm, vendorId: val || "" })} searchable clearable /> setAcqForm({ ...acqForm, status: (val as AcquisitionStatus) || "ACTIVE" }) } /> setAcqForm({ ...acqForm, notes: e.currentTarget.value })} /> {/* ---- Vendor Modal ---- */} setVendorModalOpen(false)} title="New Vendor" size="lg" > setVendorForm({ ...vendorForm, name: e.currentTarget.value })} required /> setDisposalForm({ ...disposalForm, vehicleId: val || "" })} searchable required /> setDisposalForm({ ...disposalForm, disposalDate: e.currentTarget.value }) } required />