Files
website-secure/app/stock-take/page.tsx
T
pm 9c14a19a87
DevSecOps Enterprise Pipeline / security-gate-and-deploy (push) Failing after 41s
chore: upload parte 1 - logica
2026-06-01 15:35:53 +01:00

294 lines
10 KiB
TypeScript

"use client";
import { useState } from "react";
import useSWR from "swr";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import {
ClipboardCheck,
Download,
Search,
Package,
} from "lucide-react";
import { useTableSort } from "@/hooks/use-table-sort";
import { SortableTableHead } from "@/components/sortable-table-head";
import type { Customer, Product, Component } from "@/lib/types";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export default function StockTakePage() {
const { data: customers = [] } = useSWR<Customer[]>("/api/customers", fetcher);
const { data: products = [] } = useSWR<Product[]>("/api/products", fetcher);
const [source, setSource] = useState("all");
const [selectedProducts, setSelectedProducts] = useState<Set<number>>(new Set());
const [queryKey, setQueryKey] = useState<string | null>(null);
const { data: results, isLoading } = useSWR<Component[]>(
queryKey,
fetcher
);
const { sorted, sort, toggleSort } = useTableSort(results ?? []);
function handleGenerate() {
const params = new URLSearchParams();
if (source !== "all") params.set("source", source);
if (selectedProducts.size > 0) {
params.set("productIds", Array.from(selectedProducts).join(","));
}
setQueryKey(`/api/stock-take?${params.toString()}`);
}
function toggleProduct(id: number) {
setSelectedProducts((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
}
function handleExportCSV() {
if (!sorted || sorted.length === 0) return;
const headers = ["Part Number", "Name", "Shelf Location", "Current Stock"];
const rows = sorted.map((c) => [
c.part_number,
c.name,
c.shelf_location || "",
String(c.stock_quantity ?? 0),
]);
const csvContent = [
headers.join(","),
...rows.map((row) =>
row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")
),
].join("\n");
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
const date = new Date().toISOString().slice(0, 10);
link.download = `stock-take-${date}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
const hasResults = results && results.length > 0;
return (
<div className="flex flex-col gap-6 p-6 lg:p-8">
<div>
<h1 className="text-2xl font-bold tracking-tight text-foreground">
Stock Take
</h1>
<p className="text-sm text-muted-foreground">
Select components by source and product to generate a stock take list
</p>
</div>
{/* Filters */}
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Search className="h-4 w-4" />
Filters
</CardTitle>
<CardDescription>
Choose which components to include in your stock take
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-6">
{/* Source Filter */}
<div className="flex flex-col gap-2">
<Label>Source</Label>
<Select value={source} onValueChange={setSource}>
<SelectTrigger className="w-full max-w-xs">
<SelectValue placeholder="All Sources" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Sources</SelectItem>
<SelectItem value="standard">Standard</SelectItem>
{customers.map((c) => (
<SelectItem key={c.id} value={`customer-${c.id}`}>
{c.code} - {c.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Product Filter */}
<div className="flex flex-col gap-2">
<Label>Products</Label>
<p className="text-xs text-muted-foreground">
Select products to only include components from their BOM. Leave empty for all components.
</p>
{products.length === 0 ? (
<p className="text-sm text-muted-foreground italic">No products available</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 mt-1">
{products.map((p) => (
<label
key={p.id}
className="flex items-center gap-2 rounded-md border px-3 py-2 cursor-pointer hover:bg-accent transition-colors"
>
<Checkbox
checked={selectedProducts.has(p.id)}
onCheckedChange={() => toggleProduct(p.id)}
/>
<div className="flex items-center gap-2 min-w-0">
<Package className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
<span className="text-sm font-medium truncate">{p.name}</span>
<span className="text-xs text-muted-foreground flex-shrink-0">{p.sku}</span>
</div>
</label>
))}
</div>
)}
</div>
<div className="flex items-center gap-3">
<Button onClick={handleGenerate} disabled={isLoading}>
<ClipboardCheck className="h-4 w-4 mr-2" />
{isLoading ? "Loading..." : "Generate Stock Take"}
</Button>
{selectedProducts.size > 0 && (
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedProducts(new Set())}
>
Clear products
</Button>
)}
</div>
</CardContent>
</Card>
{/* Results */}
{queryKey && (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle className="text-base flex items-center gap-2">
<ClipboardCheck className="h-4 w-4" />
Stock Take Results
</CardTitle>
<CardDescription>
{isLoading
? "Loading..."
: `${sorted.length} component${sorted.length !== 1 ? "s" : ""} found`}
</CardDescription>
</div>
{hasResults && (
<Button variant="outline" size="sm" onClick={handleExportCSV}>
<Download className="h-4 w-4 mr-2" />
Export CSV
</Button>
)}
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<SortableTableHead column="part_number" label="Part Number" sort={sort} onToggle={toggleSort} />
<SortableTableHead column="name" label="Name" sort={sort} onToggle={toggleSort} />
<SortableTableHead column="category_name" label="Category" sort={sort} onToggle={toggleSort} />
<TableHead>Source</TableHead>
<SortableTableHead column="shelf_location" label="Shelf Location" sort={sort} onToggle={toggleSort} />
<SortableTableHead column="stock_quantity" label="Current Stock" sort={sort} onToggle={toggleSort} className="text-right" />
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={6} className="py-12 text-center text-muted-foreground">
Loading components...
</TableCell>
</TableRow>
) : sorted.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="py-12 text-center text-muted-foreground">
No components match the selected filters
</TableCell>
</TableRow>
) : (
sorted.map((comp) => (
<TableRow key={comp.id}>
<TableCell className="font-mono text-xs">
{comp.part_number}
</TableCell>
<TableCell className="font-medium">
{comp.name}
</TableCell>
<TableCell>
{comp.category_name ? (
<Badge variant="secondary" className="text-xs">
{comp.category_name}
</Badge>
) : (
<span className="text-xs text-muted-foreground">-</span>
)}
</TableCell>
<TableCell>
{comp.customer_supplied ? (
<Badge variant="outline" className="text-xs border-primary/30 text-primary">
{comp.customer_name || "Customer"}
</Badge>
) : (
<span className="text-xs text-muted-foreground">Standard</span>
)}
</TableCell>
<TableCell className="font-mono text-xs text-muted-foreground">
{comp.shelf_location || "-"}
</TableCell>
<TableCell className="text-right font-mono tabular-nums">
{comp.stock_quantity ?? 0}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
)}
</div>
);
}