chore: upload parte 1 - logica
DevSecOps Enterprise Pipeline / security-gate-and-deploy (push) Failing after 41s

This commit is contained in:
2026-06-01 15:35:53 +01:00
parent a63625d228
commit 9c14a19a87
127 changed files with 22255 additions and 0 deletions
+236
View File
@@ -0,0 +1,236 @@
"use client";
import { useState, useCallback } from "react";
import useSWR from "swr";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Plus, Pencil, Trash2, ClipboardList } from "lucide-react";
import { useTableSort } from "@/hooks/use-table-sort";
import { SortableTableHead } from "@/components/sortable-table-head";
import { ProductDialog } from "@/components/product-dialog";
import { toast } from "sonner";
import { formatCurrency } from "@/lib/format";
import type { Product } from "@/lib/types";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
const statusStyles: Record<string, string> = {
draft: "bg-muted text-muted-foreground",
active: "bg-accent/15 text-accent border-accent/30",
deprecated: "bg-warning/15 text-warning border-warning/30",
archived: "bg-muted text-muted-foreground",
};
export default function ProductsPage() {
const [dialogOpen, setDialogOpen] = useState(false);
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
const [deleteTarget, setDeleteTarget] = useState<Product | null>(null);
const { data: products = [], mutate } = useSWR<Product[]>(
"/api/products",
fetcher
);
const { sorted: sortedProducts, sort, toggleSort } = useTableSort(products);
const handleSave = useCallback(
async (data: Partial<Product>) => {
const url = editingProduct
? `/api/products/${editingProduct.id}`
: "/api/products";
const method = editingProduct ? "PUT" : "POST";
const res = await fetch(url, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || "Failed to save");
}
toast.success(
editingProduct ? "Product updated" : "Product created"
);
mutate();
},
[editingProduct, mutate]
);
const handleDelete = useCallback(async () => {
if (!deleteTarget) return;
await fetch(`/api/products/${deleteTarget.id}`, { method: "DELETE" });
toast.success("Product deleted");
setDeleteTarget(null);
mutate();
}, [deleteTarget, mutate]);
function openCreate() {
setEditingProduct(null);
setDialogOpen(true);
}
function openEdit(prod: Product) {
setEditingProduct(prod);
setDialogOpen(true);
}
return (
<div className="flex flex-col gap-6 p-6 lg:p-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight text-foreground">
Products
</h1>
<p className="text-sm text-muted-foreground">
Manage your products and their bill of materials
</p>
</div>
<Button onClick={openCreate}>
<Plus className="mr-2 h-4 w-4" />
Add Product
</Button>
</div>
<div className="rounded-lg border bg-card">
<Table>
<TableHeader>
<TableRow>
<SortableTableHead column="sku" label="SKU" sort={sort} onToggle={toggleSort} className="w-[120px]" />
<SortableTableHead column="name" label="Name" sort={sort} onToggle={toggleSort} />
<SortableTableHead column="version" label="Version" sort={sort} onToggle={toggleSort} />
<SortableTableHead column="status" label="Status" sort={sort} onToggle={toggleSort} />
<SortableTableHead column="bom_count" label="BOM Items" sort={sort} onToggle={toggleSort} className="text-right" />
<SortableTableHead column="total_cost" label="BOM Cost" sort={sort} onToggle={toggleSort} className="text-right" />
<TableHead className="w-[130px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedProducts.length === 0 ? (
<TableRow>
<TableCell
colSpan={7}
className="text-center py-12 text-muted-foreground"
>
No products yet. Click &quot;Add Product&quot; to get started.
</TableCell>
</TableRow>
) : (
sortedProducts.map((prod) => (
<TableRow key={prod.id}>
<TableCell className="font-mono text-xs font-medium">
{prod.sku}
</TableCell>
<TableCell className="font-medium">{prod.name}</TableCell>
<TableCell className="text-muted-foreground">
v{prod.version}
</TableCell>
<TableCell>
<Badge
variant="outline"
className={statusStyles[prod.status] || ""}
>
{prod.status}
</Badge>
</TableCell>
<TableCell className="text-right">
{prod.bom_count || 0}
</TableCell>
<TableCell className="text-right font-mono text-sm">
{formatCurrency(Number(prod.total_cost || 0))}
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
asChild
>
<Link href={`/bom/${prod.id}`}>
<ClipboardList className="h-3.5 w-3.5" />
<span className="sr-only">View BOM</span>
</Link>
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => openEdit(prod)}
>
<Pencil className="h-3.5 w-3.5" />
<span className="sr-only">Edit</span>
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive hover:text-destructive"
onClick={() => setDeleteTarget(prod)}
>
<Trash2 className="h-3.5 w-3.5" />
<span className="sr-only">Delete</span>
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<ProductDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
product={editingProduct}
onSave={handleSave}
/>
<AlertDialog
open={!!deleteTarget}
onOpenChange={() => setDeleteTarget(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Product</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete{" "}
<strong>{deleteTarget?.name}</strong> ({deleteTarget?.sku})? This
will also delete all associated BOM items.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}