import { useState, useMemo, useCallback } from "react"; import { createFileRoute, Link } from "@tanstack/react-router"; import { Search, Plus, Pencil, Trash2, Package, Star } from "lucide-react"; import { GridTable } from "#/components/GridTable"; import type { GridTableColumn } from "#/components/GridTable"; import { Button, buttonVariants } from "#/components/ui"; import { useListStockBatches, useRemoveStockBatch, trpc } from "shared-react"; interface StockRow { id: number; product: { id: number; name: string; brand: string }; arrived: string; batch_no: string; mfg: string; expiry: string; rack: { id: number; name: string } | null; distributor: { id: number; agency: string } | null; quantity: number; is_default: boolean; } function daysUntil(expiry: string): number { const today = new Date(); today.setHours(0, 0, 0, 0); const exp = new Date(expiry + "T00:00:00"); return Math.ceil((exp.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); } function fmtDate(d: string): string { const date = new Date(d + "T00:00:00"); return date.toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric" }); } function daysBadge(days: number) { if (days < 0) return Expired; if (days <= 30) return {days} days; if (days <= 90) return {days} days; return {days} days; } function makeColumns( onDelete: (row: StockRow) => void, ): GridTableColumn[] { return [ { id: "product", header: "Product", cell: ({ row }) => (
{row.product.name}
{row.product.brand}
), }, { id: "arrived", header: "Arrived Date", cell: ({ row }) => {fmtDate(row.arrived)}, }, { id: "batch_no", header: "Batch No", cell: ({ row }) => ( {row.batch_no} ), }, { id: "quantity", header: "Qty", size: 60, cell: ({ row }) => ( {row.quantity} ), }, { id: "is_default", header: "Default", size: 60, cell: ({ row }) => ( {row.is_default ? ( ) : ( )} ), }, { id: "mfg", header: "Manufacture Date", cell: ({ row }) => {fmtDate(row.mfg)}, }, { id: "expiry", header: "Expiry Date", cell: ({ row }) => { const days = daysUntil(row.expiry); return ( {fmtDate(row.expiry)} ); }, }, { id: "days", header: "Days Remaining", cell: ({ row }) => daysBadge(daysUntil(row.expiry)), }, { id: "rack", header: "Rack", cell: ({ row }) => {row.rack?.name || "—"}, }, { id: "distributor", header: "Distributor", cell: ({ row }) => {row.distributor?.agency || "—"}, }, { id: "actions", header: "Actions", size: 90, cell: ({ row }) => (
View
), }, ]; } export const Route = createFileRoute("/stock/")({ component: StockIndexPage, staticData: { title: "Stock Batches", subtitle: "Track inventory batches by expiry and rack location", }, }); function StockIndexPage() { const [searchQuery, setSearchQuery] = useState(""); const { data: batches, isLoading, error } = useListStockBatches(); const removeMutation = useRemoveStockBatch(); const utils = trpc.useUtils(); const handleDelete = useCallback( (row: StockRow) => { if (!confirm(`Delete batch ${row.batch_no}?`)) return; removeMutation.mutate( { id: row.id }, { onSuccess: () => utils.stock.list.invalidate() }, ); }, [removeMutation, utils], ); const columns = useMemo(() => makeColumns(handleDelete), [handleDelete]); const filtered = useMemo(() => { const q = searchQuery.toLowerCase().trim(); if (!q) return batches ?? []; return (batches ?? []).filter((b) => { const text = `${b.product.name} ${b.product.brand} ${b.batch_no} ${b.rack?.name || ""} ${b.distributor?.agency || ""}`; return text.toLowerCase().includes(q); }); }, [searchQuery, batches]); if (isLoading) return
Loading stock batches...
; if (error) return
Failed to load stock batches.
; return (
setSearchQuery(e.target.value)} placeholder="Search by product, brand, batch, or rack..." className="bg-transparent border-none outline-none text-sm text-slate-900 w-full placeholder:text-slate-400" />
Add Stock

No stock batches found

{searchQuery ? "No batches match your search." : "Add your first stock batch to get started."}

} /> ); }