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
+307
View File
@@ -0,0 +1,307 @@
"use client";
import { useState } from "react";
import useSWR from "swr";
import Link from "next/link";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
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,
Search,
Pencil,
Trash2,
Users,
Euro,
FileText,
ExternalLink,
} from "lucide-react";
import { useTableSort } from "@/hooks/use-table-sort";
import { SortableTableHead } from "@/components/sortable-table-head";
import { CustomerDialog } from "@/components/customer-dialog";
import { toast } from "sonner";
import { formatCurrency } from "@/lib/format";
import type { Customer } from "@/lib/types";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export default function CustomersPage() {
const [search, setSearch] = useState("");
const [dialogOpen, setDialogOpen] = useState(false);
const [editingCustomer, setEditingCustomer] = useState<Customer | null>(null);
const [deleteTarget, setDeleteTarget] = useState<Customer | null>(null);
const { data: customers = [], mutate } = useSWR<Customer[]>(
`/api/customers?search=${encodeURIComponent(search)}`,
fetcher
);
const { sorted: sortedCustomers, sort, toggleSort } = useTableSort(customers);
function openEdit(customer: Customer) {
setEditingCustomer(customer);
setDialogOpen(true);
}
function openCreate() {
setEditingCustomer(null);
setDialogOpen(true);
}
async function handleDelete() {
if (!deleteTarget) return;
try {
const res = await fetch(`/api/customers/${deleteTarget.id}`, {
method: "DELETE",
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || "Failed to delete");
}
toast.success("Customer deleted");
mutate();
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Failed to delete customer"
);
} finally {
setDeleteTarget(null);
}
}
const totalCustomers = customers.length;
const totalRevenue = customers.reduce(
(sum, c) => sum + Number(c.total_revenue || 0),
0
);
const totalOrders = customers.reduce(
(sum, c) => sum + Number(c.order_count || 0),
0
);
return (
<div className="flex flex-col gap-6 p-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight text-foreground">
Customers & Orders
</h1>
<p className="text-sm text-muted-foreground">
Manage customers and their purchase orders
</p>
</div>
<Button onClick={openCreate}>
<Plus className="mr-2 h-4 w-4" />
Add Customer
</Button>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Customers
</CardTitle>
<Users className="h-4 w-4 text-primary" />
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">{totalCustomers}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Orders
</CardTitle>
<FileText className="h-4 w-4 text-primary" />
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">{totalOrders}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Revenue
</CardTitle>
<Euro className="h-4 w-4 text-accent" />
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">
{formatCurrency(totalRevenue)}
</p>
</CardContent>
</Card>
</div>
{/* Search */}
<div className="relative max-w-sm">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search customers..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10"
/>
</div>
{/* Customer Table */}
<Card>
<CardContent className="p-0">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<SortableTableHead column="code" label="Code" sort={sort} onToggle={toggleSort} />
<SortableTableHead column="name" label="Company Name" sort={sort} onToggle={toggleSort} />
<SortableTableHead column="contact_person" label="Contact Person" sort={sort} onToggle={toggleSort} />
<SortableTableHead column="email" label="Email" sort={sort} onToggle={toggleSort} />
<SortableTableHead column="phone" label="Phone" sort={sort} onToggle={toggleSort} />
<SortableTableHead column="order_count" label="Orders" sort={sort} onToggle={toggleSort} className="text-right" />
<SortableTableHead column="total_revenue" label="Revenue" sort={sort} onToggle={toggleSort} className="text-right" />
<TableHead className="w-[140px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedCustomers.length === 0 ? (
<TableRow>
<TableCell
colSpan={8}
className="py-12 text-center text-muted-foreground"
>
No customers found. Click &quot;Add Customer&quot; to
create one.
</TableCell>
</TableRow>
) : (
sortedCustomers.map((customer) => (
<TableRow key={customer.id}>
<TableCell>
<Badge variant="outline" className="font-mono text-xs">
{customer.code}
</Badge>
</TableCell>
<TableCell className="font-medium">
<Link
href={`/customers/${customer.id}`}
className="flex items-center gap-1.5 text-primary hover:underline"
>
{customer.name}
<ExternalLink className="h-3 w-3" />
</Link>
</TableCell>
<TableCell className="text-muted-foreground">
{customer.contact_person || "-"}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{customer.email || "-"}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{customer.phone || "-"}
</TableCell>
<TableCell className="text-right font-mono text-sm">
{Number(customer.order_count || 0)}
</TableCell>
<TableCell className="text-right font-mono text-sm">
{formatCurrency(Number(customer.total_revenue || 0))}
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
asChild
>
<Link href={`/customers/${customer.id}`}>
<FileText className="h-3.5 w-3.5" />
<span className="sr-only">View Orders</span>
</Link>
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => openEdit(customer)}
>
<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(customer)}
>
<Trash2 className="h-3.5 w-3.5" />
<span className="sr-only">Delete</span>
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
<CustomerDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
customer={editingCustomer}
onSaved={() => mutate()}
/>
<AlertDialog
open={!!deleteTarget}
onOpenChange={() => setDeleteTarget(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Customer</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete &quot;{deleteTarget?.name}&quot;?
This action cannot be undone.
</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>
);
}