123 lines
5.6 KiB
TypeScript
123 lines
5.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { Trash2, ShoppingCart } from "lucide-react";
|
|
import { getCart, saveCart, clearCart, type CartItem } from "@/lib/cart";
|
|
import { formatCurrency } from "@/lib/utils";
|
|
import Link from "next/link";
|
|
|
|
export function CartView() {
|
|
const router = useRouter();
|
|
const [items, setItems] = useState<CartItem[]>([]);
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setItems(getCart());
|
|
setMounted(true);
|
|
const handler = () => setItems(getCart());
|
|
window.addEventListener("cart-updated", handler);
|
|
return () => window.removeEventListener("cart-updated", handler);
|
|
}, []);
|
|
|
|
function updateQty(idx: number, qty: number) {
|
|
const next = items.map((item, i) => i === idx ? { ...item, quantity: qty } : item);
|
|
setItems(next); saveCart(next);
|
|
}
|
|
|
|
function remove(idx: number) {
|
|
const next = items.filter((_, i) => i !== idx);
|
|
setItems(next); saveCart(next);
|
|
}
|
|
|
|
function createPO() {
|
|
const encoded = encodeURIComponent(JSON.stringify(items));
|
|
clearCart();
|
|
setItems([]);
|
|
router.push(`/po/new?cart=${encoded}`);
|
|
}
|
|
|
|
if (!mounted) return null;
|
|
|
|
if (items.length === 0) {
|
|
return (
|
|
<div className="rounded-lg border border-neutral-200 bg-white p-12 text-center">
|
|
<ShoppingCart className="h-12 w-12 text-neutral-300 mx-auto mb-4" />
|
|
<p className="text-neutral-500 font-medium">Your cart is empty</p>
|
|
<p className="text-sm text-neutral-400 mt-1 mb-6">Browse Items or Vendors to add line items</p>
|
|
<div className="flex gap-3 justify-center">
|
|
<Link href="/inventory/items" className="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50">Browse Items</Link>
|
|
<Link href="/inventory/vendors" className="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50">Browse Vendors</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const total = items.reduce((s, i) => s + i.quantity * i.unitPrice, 0);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="rounded-lg border border-neutral-200 bg-white overflow-hidden">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-neutral-50 border-b border-neutral-200">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left font-medium text-neutral-600">Item</th>
|
|
<th className="px-4 py-3 text-left font-medium text-neutral-600">Vendor</th>
|
|
<th className="px-4 py-3 text-right font-medium text-neutral-600">Unit Price</th>
|
|
<th className="px-4 py-3 text-right font-medium text-neutral-600">Qty</th>
|
|
<th className="px-4 py-3 text-right font-medium text-neutral-600">Total</th>
|
|
<th className="px-4 py-3 w-8" />
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-neutral-100">
|
|
{items.map((item, i) => (
|
|
<tr key={i}>
|
|
<td className="px-4 py-3">
|
|
<p className="font-medium text-neutral-900">{item.name}</p>
|
|
{item.description && <p className="text-xs text-neutral-500">{item.description}</p>}
|
|
</td>
|
|
<td className="px-4 py-3 text-neutral-600">{item.vendorName ?? <span className="italic text-neutral-400">Not specified</span>}</td>
|
|
<td className="px-4 py-3 text-right text-neutral-700">{item.unitPrice > 0 ? formatCurrency(item.unitPrice) : <span className="italic text-neutral-400">TBD</span>}</td>
|
|
<td className="px-4 py-3 text-right">
|
|
<input
|
|
type="number" min="1" step="any"
|
|
value={item.quantity}
|
|
onChange={(e) => updateQty(i, parseFloat(e.target.value) || 1)}
|
|
className="w-20 rounded border border-neutral-200 px-2 py-1 text-sm text-right"
|
|
/>
|
|
</td>
|
|
<td className="px-4 py-3 text-right font-medium text-neutral-900">
|
|
{item.unitPrice > 0 ? formatCurrency(item.quantity * item.unitPrice) : "—"}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<button onClick={() => remove(i)} className="text-neutral-400 hover:text-danger"><Trash2 className="h-4 w-4" /></button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
<tfoot className="border-t border-neutral-200 bg-neutral-50">
|
|
<tr>
|
|
<td colSpan={4} className="px-4 py-3 text-right text-sm font-semibold text-neutral-900">Estimated Total (excl. GST)</td>
|
|
<td className="px-4 py-3 text-right text-sm font-semibold text-neutral-900">{formatCurrency(total)}</td>
|
|
<td />
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<button onClick={() => { clearCart(); setItems([]); }} className="text-sm text-danger-600 hover:underline">Clear cart</button>
|
|
<div className="flex gap-3">
|
|
<Link href="/inventory/items" className="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50">
|
|
+ Add more items
|
|
</Link>
|
|
<button onClick={createPO} className="rounded-lg bg-primary-600 px-5 py-2 text-sm font-semibold text-white hover:bg-primary-700">
|
|
Create Purchase Order →
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-xs text-neutral-400">Prices shown are last known prices. Final amounts are set in the PO form.</p>
|
|
</div>
|
|
);
|
|
}
|