chore: upload parte 1 - logica
DevSecOps Enterprise Pipeline / security-gate-and-deploy (push) Failing after 41s
DevSecOps Enterprise Pipeline / security-gate-and-deploy (push) Failing after 41s
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user