Adds complete initial codebase for the RJ Bulk Supplies fuel delivery backoffice: - Authentication pages and protected route layouts - Customer, supplier, fuel purchase, and delivery note management - Custom reporting and customer statement sending tools - Full UI component library and Tailwind styling - Server actions, API routes, and PocketBase integration - Development scripts and project configuration updates - Updated README and package metadata
124 lines
4.6 KiB
TypeScript
124 lines
4.6 KiB
TypeScript
"use client";
|
||
|
||
import { useActionState, useEffect, useRef, useState } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Select } from "@/components/ui/select";
|
||
import { createFuelPurchase, updateFuelPurchase, type ActionResult } from "@/actions/fuel-purchases";
|
||
import { toast } from "sonner";
|
||
import type { FuelPurchase, Supplier } from "@/lib/types";
|
||
import { formatCurrency, formatNumber } from "@/lib/utils";
|
||
|
||
const initial: ActionResult = { ok: false, error: "" };
|
||
|
||
function todayISO() {
|
||
return new Date().toISOString().slice(0, 10);
|
||
}
|
||
|
||
export function FuelPurchaseForm({ suppliers, purchase }: { suppliers: Supplier[]; purchase?: FuelPurchase }) {
|
||
const router = useRouter();
|
||
const isEdit = !!purchase;
|
||
const bound = isEdit ? updateFuelPurchase.bind(null, purchase!.id) : createFuelPurchase;
|
||
const [state, action, pending] = useActionState(bound, initial);
|
||
const ref = useRef<HTMLFormElement>(null);
|
||
const [supplier, setSupplier] = useState(purchase?.supplier || "");
|
||
const [qty, setQty] = useState<number | "">(purchase?.purchase_qty ?? "");
|
||
const [price, setPrice] = useState<number | "">(purchase?.purchase_price ?? "");
|
||
|
||
useEffect(() => {
|
||
if (state.ok) {
|
||
toast.success(isEdit ? "Purchase updated" : "Purchase created");
|
||
if (!isEdit) {
|
||
ref.current?.reset();
|
||
setQty("");
|
||
setPrice("");
|
||
} else {
|
||
router.push("/fuel-purchases");
|
||
}
|
||
} else if (state.error) { toast.error(state.error); }
|
||
}, [state, isEdit, router]);
|
||
|
||
const total = qty && price ? Number(qty) * Number(price) : 0;
|
||
|
||
return (
|
||
<Card className="max-w-2xl">
|
||
<CardHeader>
|
||
<CardTitle>{isEdit ? "Edit fuel purchase" : "Add fuel purchase"}</CardTitle>
|
||
</CardHeader>
|
||
<form ref={ref} action={action}>
|
||
<CardContent className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="supplier">Supplier *</Label>
|
||
<Select
|
||
id="supplier"
|
||
name="supplier"
|
||
value={supplier}
|
||
onChange={(e) => setSupplier(e.target.value)}
|
||
required
|
||
options={suppliers.map((s) => ({ value: s.id, label: s.name }))}
|
||
placeholder="Select a supplier"
|
||
/>
|
||
</div>
|
||
<div className="grid gap-4 sm:grid-cols-3">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="purchase_qty">Qty (litres) *</Label>
|
||
<Input
|
||
id="purchase_qty"
|
||
name="purchase_qty"
|
||
type="number"
|
||
step="0.01"
|
||
min="0.01"
|
||
value={qty}
|
||
onChange={(e) => setQty(e.target.value === "" ? "" : Number(e.target.value))}
|
||
required
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="purchase_price">Price / L *</Label>
|
||
<Input
|
||
id="purchase_price"
|
||
name="purchase_price"
|
||
type="number"
|
||
step="0.01"
|
||
min="0.01"
|
||
value={price}
|
||
onChange={(e) => setPrice(e.target.value === "" ? "" : Number(e.target.value))}
|
||
required
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>Total</Label>
|
||
<div className="h-9 rounded-md border bg-muted/50 px-3 py-1.5 text-sm font-medium">
|
||
{formatCurrency(total)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="purchase_date">Purchase date *</Label>
|
||
<Input
|
||
id="purchase_date"
|
||
name="purchase_date"
|
||
type="date"
|
||
defaultValue={(purchase?.purchase_date || "").slice(0, 10) || todayISO()}
|
||
required
|
||
/>
|
||
</div>
|
||
{qty && price && (
|
||
<p className="text-xs text-muted-foreground">
|
||
{formatNumber(Number(qty))} L × {formatCurrency(Number(price))}/L = {formatCurrency(total)}
|
||
</p>
|
||
)}
|
||
</CardContent>
|
||
<CardFooter className="flex justify-between">
|
||
<Button type="button" variant="ghost" onClick={() => router.back()}>Cancel</Button>
|
||
<Button type="submit" disabled={pending}>
|
||
{pending ? "Saving…" : isEdit ? "Update purchase" : "Create purchase"}
|
||
</Button>
|
||
</CardFooter>
|
||
</form>
|
||
</Card>
|
||
);
|
||
}
|