mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 16:35:42 +00:00
666 lines
22 KiB
TypeScript
666 lines
22 KiB
TypeScript
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 = <T extends Record<string, unknown>>(obj: T): Partial<T> =>
|
|
Object.fromEntries(
|
|
Object.entries(obj).filter(([, v]) => v !== "" && v !== undefined && v !== null),
|
|
) as Partial<T>;
|
|
|
|
const emptyAcquisition = {
|
|
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<string>("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 (
|
|
<Container size="xl" py="xl" px="lg">
|
|
<Breadcrumbs items={[{ label: "Fleet" }, { label: "Procurement" }]} />
|
|
|
|
<Group justify="space-between" mb="lg">
|
|
<Title order={1}>Procurement & Assets</Title>
|
|
</Group>
|
|
|
|
<Tabs value={tab} onChange={(val) => setTab(val || "acquisitions")}>
|
|
<Tabs.List mb="lg">
|
|
<Tabs.Tab value="acquisitions">Acquisitions</Tabs.Tab>
|
|
<Tabs.Tab value="vendors">Vendors</Tabs.Tab>
|
|
<Tabs.Tab value="disposals">Disposals</Tabs.Tab>
|
|
</Tabs.List>
|
|
|
|
{/* ---- Acquisitions ---- */}
|
|
<Tabs.Panel value="acquisitions">
|
|
<Group justify="flex-end" mb="md">
|
|
<Button
|
|
leftSection={<Plus size={16} />}
|
|
onClick={() => setAcqModalOpen(true)}
|
|
color="edr-green"
|
|
>
|
|
New Acquisition
|
|
</Button>
|
|
</Group>
|
|
<Card withBorder>
|
|
<Table striped highlightOnHover>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>Vehicle</Table.Th>
|
|
<Table.Th>Type</Table.Th>
|
|
<Table.Th>Date</Table.Th>
|
|
<Table.Th align="right">Cost</Table.Th>
|
|
<Table.Th>Status</Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{loadingAcquisitions ? (
|
|
<Table.Tr>
|
|
<Table.Td colSpan={5}>
|
|
<Group justify="center" py="md">
|
|
<Loader size="sm" />
|
|
</Group>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
) : acquisitions.length === 0 ? (
|
|
<Table.Tr>
|
|
<Table.Td colSpan={5}>
|
|
<Text c="dimmed" ta="center" py="md">
|
|
No acquisitions recorded yet.
|
|
</Text>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
) : null}
|
|
{acquisitions.map((a: AssetAcquisition) => (
|
|
<Table.Tr key={a.id}>
|
|
<Table.Td>{vehicleLabel(a.vehicle, a.vehicleId)}</Table.Td>
|
|
<Table.Td>
|
|
<Badge size="sm" color={typeBadgeColor(a.acquisitionType)}>
|
|
{a.acquisitionType}
|
|
</Badge>
|
|
</Table.Td>
|
|
<Table.Td>{new Date(a.acquisitionDate).toLocaleDateString()}</Table.Td>
|
|
<Table.Td align="right">{money(a.cost)}</Table.Td>
|
|
<Table.Td>
|
|
<Badge size="sm" color={statusBadgeColor(a.status)}>
|
|
{a.status}
|
|
</Badge>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
</Table.Tbody>
|
|
</Table>
|
|
</Card>
|
|
</Tabs.Panel>
|
|
|
|
{/* ---- Vendors ---- */}
|
|
<Tabs.Panel value="vendors">
|
|
<Group justify="flex-end" mb="md">
|
|
<Button
|
|
leftSection={<Plus size={16} />}
|
|
onClick={() => setVendorModalOpen(true)}
|
|
color="edr-green"
|
|
>
|
|
New Vendor
|
|
</Button>
|
|
</Group>
|
|
<Card withBorder>
|
|
<Table striped highlightOnHover>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>Name</Table.Th>
|
|
<Table.Th>Type</Table.Th>
|
|
<Table.Th>Contact</Table.Th>
|
|
<Table.Th>Phone</Table.Th>
|
|
<Table.Th>Email</Table.Th>
|
|
<Table.Th>Active</Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{loadingVendors ? (
|
|
<Table.Tr>
|
|
<Table.Td colSpan={6}>
|
|
<Group justify="center" py="md">
|
|
<Loader size="sm" />
|
|
</Group>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
) : vendors.length === 0 ? (
|
|
<Table.Tr>
|
|
<Table.Td colSpan={6}>
|
|
<Text c="dimmed" ta="center" py="md">
|
|
No vendors added yet.
|
|
</Text>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
) : null}
|
|
{vendors.map((v: Vendor) => (
|
|
<Table.Tr key={v.id}>
|
|
<Table.Td>{v.name}</Table.Td>
|
|
<Table.Td>{v.type ? <Badge size="sm">{v.type}</Badge> : "—"}</Table.Td>
|
|
<Table.Td>{v.contactPerson || "—"}</Table.Td>
|
|
<Table.Td>{v.phone || "—"}</Table.Td>
|
|
<Table.Td>{v.email || "—"}</Table.Td>
|
|
<Table.Td>
|
|
<Badge size="sm" color={v.isActive ? "green" : "gray"}>
|
|
{v.isActive ? "Active" : "Inactive"}
|
|
</Badge>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
</Table.Tbody>
|
|
</Table>
|
|
</Card>
|
|
</Tabs.Panel>
|
|
|
|
{/* ---- Disposals ---- */}
|
|
<Tabs.Panel value="disposals">
|
|
<Group justify="flex-end" mb="md">
|
|
<Button
|
|
leftSection={<Plus size={16} />}
|
|
onClick={() => setDisposalModalOpen(true)}
|
|
color="edr-green"
|
|
>
|
|
New Disposal
|
|
</Button>
|
|
</Group>
|
|
<Card withBorder>
|
|
<Table striped highlightOnHover>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
<Table.Th>Vehicle</Table.Th>
|
|
<Table.Th>Method</Table.Th>
|
|
<Table.Th>Date</Table.Th>
|
|
<Table.Th align="right">Sale Price</Table.Th>
|
|
<Table.Th>Buyer</Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{loadingDisposals ? (
|
|
<Table.Tr>
|
|
<Table.Td colSpan={5}>
|
|
<Group justify="center" py="md">
|
|
<Loader size="sm" />
|
|
</Group>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
) : disposals.length === 0 ? (
|
|
<Table.Tr>
|
|
<Table.Td colSpan={5}>
|
|
<Text c="dimmed" ta="center" py="md">
|
|
No disposals recorded yet.
|
|
</Text>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
) : null}
|
|
{disposals.map((d: AssetDisposal) => (
|
|
<Table.Tr key={d.id}>
|
|
<Table.Td>{d.vehicleId}</Table.Td>
|
|
<Table.Td>
|
|
<Badge size="sm">{d.method}</Badge>
|
|
</Table.Td>
|
|
<Table.Td>{new Date(d.disposalDate).toLocaleDateString()}</Table.Td>
|
|
<Table.Td align="right">{money(d.salePrice)}</Table.Td>
|
|
<Table.Td>{d.buyer || "—"}</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
</Table.Tbody>
|
|
</Table>
|
|
</Card>
|
|
</Tabs.Panel>
|
|
</Tabs>
|
|
|
|
{/* ---- Acquisition Modal ---- */}
|
|
<Modal
|
|
opened={acqModalOpen}
|
|
onClose={() => setAcqModalOpen(false)}
|
|
title="New Acquisition"
|
|
size="lg"
|
|
>
|
|
<Stack gap="md">
|
|
<Select
|
|
label="Vehicle"
|
|
placeholder="Select vehicle"
|
|
data={vehicleOptions}
|
|
value={acqForm.vehicleId}
|
|
onChange={(val) => setAcqForm({ ...acqForm, vehicleId: val || "" })}
|
|
searchable
|
|
clearable
|
|
/>
|
|
<Select
|
|
label="Vendor"
|
|
placeholder="Select vendor"
|
|
data={vendorOptions}
|
|
value={acqForm.vendorId}
|
|
onChange={(val) => setAcqForm({ ...acqForm, vendorId: val || "" })}
|
|
searchable
|
|
clearable
|
|
/>
|
|
<Select
|
|
label="Acquisition Type"
|
|
data={ACQUISITION_TYPES}
|
|
value={acqForm.acquisitionType}
|
|
onChange={(val) =>
|
|
setAcqForm({ ...acqForm, acquisitionType: (val as AcquisitionType) || "PURCHASE" })
|
|
}
|
|
required
|
|
/>
|
|
<TextInput
|
|
label="Acquisition Date"
|
|
type="date"
|
|
value={acqForm.acquisitionDate}
|
|
onChange={(e) => setAcqForm({ ...acqForm, acquisitionDate: e.currentTarget.value })}
|
|
required
|
|
/>
|
|
<NumberInput
|
|
label="Cost"
|
|
placeholder="0.00"
|
|
value={acqForm.cost}
|
|
onChange={(val) => setAcqForm({ ...acqForm, cost: val as number | undefined })}
|
|
decimalScale={2}
|
|
min={0}
|
|
/>
|
|
<NumberInput
|
|
label="Useful Life (months)"
|
|
placeholder="Optional"
|
|
value={acqForm.usefulLifeMonths}
|
|
onChange={(val) =>
|
|
setAcqForm({ ...acqForm, usefulLifeMonths: val as number | undefined })
|
|
}
|
|
decimalScale={0}
|
|
min={0}
|
|
/>
|
|
<NumberInput
|
|
label="Salvage Value"
|
|
placeholder="0.00"
|
|
value={acqForm.salvageValue}
|
|
onChange={(val) => setAcqForm({ ...acqForm, salvageValue: val as number | undefined })}
|
|
decimalScale={2}
|
|
min={0}
|
|
/>
|
|
<TextInput
|
|
label="Lease Start"
|
|
type="date"
|
|
value={acqForm.leaseStart}
|
|
onChange={(e) => setAcqForm({ ...acqForm, leaseStart: e.currentTarget.value })}
|
|
/>
|
|
<TextInput
|
|
label="Lease End"
|
|
type="date"
|
|
value={acqForm.leaseEnd}
|
|
onChange={(e) => setAcqForm({ ...acqForm, leaseEnd: e.currentTarget.value })}
|
|
/>
|
|
<NumberInput
|
|
label="Monthly Payment"
|
|
placeholder="0.00"
|
|
value={acqForm.monthlyPayment}
|
|
onChange={(val) =>
|
|
setAcqForm({ ...acqForm, monthlyPayment: val as number | undefined })
|
|
}
|
|
decimalScale={2}
|
|
min={0}
|
|
/>
|
|
<Select
|
|
label="Status"
|
|
data={ACQUISITION_STATUSES}
|
|
value={acqForm.status}
|
|
onChange={(val) =>
|
|
setAcqForm({ ...acqForm, status: (val as AcquisitionStatus) || "ACTIVE" })
|
|
}
|
|
/>
|
|
<TextInput
|
|
label="Notes"
|
|
placeholder="Optional notes"
|
|
value={acqForm.notes}
|
|
onChange={(e) => setAcqForm({ ...acqForm, notes: e.currentTarget.value })}
|
|
/>
|
|
<Group justify="flex-end">
|
|
<Button variant="light" onClick={() => setAcqModalOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={() => createAcquisition.mutate()}
|
|
loading={createAcquisition.isPending}
|
|
disabled={!acqForm.acquisitionDate}
|
|
>
|
|
Save Acquisition
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
</Modal>
|
|
|
|
{/* ---- Vendor Modal ---- */}
|
|
<Modal
|
|
opened={vendorModalOpen}
|
|
onClose={() => setVendorModalOpen(false)}
|
|
title="New Vendor"
|
|
size="lg"
|
|
>
|
|
<Stack gap="md">
|
|
<TextInput
|
|
label="Name"
|
|
placeholder="Vendor name"
|
|
value={vendorForm.name}
|
|
onChange={(e) => setVendorForm({ ...vendorForm, name: e.currentTarget.value })}
|
|
required
|
|
/>
|
|
<Select
|
|
label="Type"
|
|
placeholder="Select type"
|
|
data={VENDOR_TYPES}
|
|
value={vendorForm.type || null}
|
|
onChange={(val) => setVendorForm({ ...vendorForm, type: (val as VendorType) || "" })}
|
|
clearable
|
|
/>
|
|
<TextInput
|
|
label="Contact Person"
|
|
placeholder="Optional"
|
|
value={vendorForm.contactPerson}
|
|
onChange={(e) => setVendorForm({ ...vendorForm, contactPerson: e.currentTarget.value })}
|
|
/>
|
|
<TextInput
|
|
label="Phone"
|
|
placeholder="Optional"
|
|
value={vendorForm.phone}
|
|
onChange={(e) => setVendorForm({ ...vendorForm, phone: e.currentTarget.value })}
|
|
/>
|
|
<TextInput
|
|
label="Email"
|
|
placeholder="Optional"
|
|
value={vendorForm.email}
|
|
onChange={(e) => setVendorForm({ ...vendorForm, email: e.currentTarget.value })}
|
|
/>
|
|
<TextInput
|
|
label="Address"
|
|
placeholder="Optional"
|
|
value={vendorForm.address}
|
|
onChange={(e) => setVendorForm({ ...vendorForm, address: e.currentTarget.value })}
|
|
/>
|
|
<Switch
|
|
label="Active"
|
|
checked={vendorForm.isActive}
|
|
onChange={(e) => setVendorForm({ ...vendorForm, isActive: e.currentTarget.checked })}
|
|
/>
|
|
<Group justify="flex-end">
|
|
<Button variant="light" onClick={() => setVendorModalOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={() => createVendor.mutate()}
|
|
loading={createVendor.isPending}
|
|
disabled={!vendorForm.name}
|
|
>
|
|
Save Vendor
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
</Modal>
|
|
|
|
{/* ---- Disposal Modal ---- */}
|
|
<Modal
|
|
opened={disposalModalOpen}
|
|
onClose={() => setDisposalModalOpen(false)}
|
|
title="New Disposal"
|
|
size="lg"
|
|
>
|
|
<Stack gap="md">
|
|
<Select
|
|
label="Vehicle"
|
|
placeholder="Select vehicle"
|
|
data={vehicleOptions}
|
|
value={disposalForm.vehicleId}
|
|
onChange={(val) => setDisposalForm({ ...disposalForm, vehicleId: val || "" })}
|
|
searchable
|
|
required
|
|
/>
|
|
<TextInput
|
|
label="Disposal Date"
|
|
type="date"
|
|
value={disposalForm.disposalDate}
|
|
onChange={(e) =>
|
|
setDisposalForm({ ...disposalForm, disposalDate: e.currentTarget.value })
|
|
}
|
|
required
|
|
/>
|
|
<Select
|
|
label="Method"
|
|
data={DISPOSAL_METHODS}
|
|
value={disposalForm.method}
|
|
onChange={(val) =>
|
|
setDisposalForm({ ...disposalForm, method: (val as DisposalMethod) || "SALE" })
|
|
}
|
|
required
|
|
/>
|
|
<NumberInput
|
|
label="Sale Price"
|
|
placeholder="0.00"
|
|
value={disposalForm.salePrice}
|
|
onChange={(val) =>
|
|
setDisposalForm({ ...disposalForm, salePrice: val as number | undefined })
|
|
}
|
|
decimalScale={2}
|
|
min={0}
|
|
/>
|
|
<TextInput
|
|
label="Buyer"
|
|
placeholder="Optional"
|
|
value={disposalForm.buyer}
|
|
onChange={(e) => setDisposalForm({ ...disposalForm, buyer: e.currentTarget.value })}
|
|
/>
|
|
<TextInput
|
|
label="Notes"
|
|
placeholder="Optional notes"
|
|
value={disposalForm.notes}
|
|
onChange={(e) => setDisposalForm({ ...disposalForm, notes: e.currentTarget.value })}
|
|
/>
|
|
<Group justify="flex-end">
|
|
<Button variant="light" onClick={() => setDisposalModalOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={() => createDisposal.mutate()}
|
|
loading={createDisposal.isPending}
|
|
disabled={!disposalForm.vehicleId || !disposalForm.disposalDate}
|
|
>
|
|
Save Disposal
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
</Modal>
|
|
</Container>
|
|
);
|
|
}
|