191 lines
9.0 KiB
TypeScript
191 lines
9.0 KiB
TypeScript
"use client";
|
||
|
||
import {useState, useEffect, useMemo, useCallback} from "react";
|
||
import {Button} from "@/components/ui/button";
|
||
import {Input} from "@/components/ui/input";
|
||
import {Card, CardContent} from "@/components/ui/card";
|
||
import {
|
||
Pagination,
|
||
PaginationContent,
|
||
PaginationItem,
|
||
PaginationNext,
|
||
PaginationPrevious
|
||
} from "@/components/ui/pagination";
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow
|
||
} from "@/components/ui/table";
|
||
import {motion} from "framer-motion";
|
||
import {ArrowRight} from "lucide-react";
|
||
import {NewCustomerModal} from "@/components/customers/modal/NewCustomerModal";
|
||
import {Customer} from "@/services/customers/entities/customer";
|
||
import Link from "next/link";
|
||
import {useErrorHandler} from "@/components/error-boundary";
|
||
import {showError} from "@/lib/ui/showToast";
|
||
|
||
export default function CustomersPage() {
|
||
const [customers, setCustomers] = useState<Customer[]>([]);
|
||
const [search, setSearch] = useState("");
|
||
const [loading, setLoading] = useState(true);
|
||
const [page, setPage] = useState(1);
|
||
const pageSize = 15;
|
||
const handleError = useErrorHandler();
|
||
|
||
// Wrap the loadCustomers function with useCallback
|
||
const loadCustomers = useCallback(async () => {
|
||
setLoading(true);
|
||
try {
|
||
const response = await fetch('/api/customers');
|
||
if (!response.ok) {
|
||
showError("Failed to fetch customers data");
|
||
throw new Error(`Failed to fetch customers: ${response.statusText}`);
|
||
}
|
||
const data = await response.json();
|
||
setCustomers(data);
|
||
} catch (error) {
|
||
showError("Failed to fetch customers data (1)");
|
||
handleError(error);
|
||
setCustomers([]);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [handleError]); // Include handleError as a dependency
|
||
|
||
useEffect(() => {
|
||
loadCustomers();
|
||
}, [loadCustomers]); // Add loadCustomers to the dependency array
|
||
|
||
const filtered = useMemo(() => {
|
||
if (customers.length === 0) return [];
|
||
|
||
return customers.filter(
|
||
(c) =>
|
||
c.name.toLowerCase().includes(search.toLowerCase()) ||
|
||
c.email.toLowerCase().includes(search.toLowerCase()) ||
|
||
c.companyName.toLowerCase().includes(search.toLowerCase()) ||
|
||
c.street.toLowerCase().includes(search.toLowerCase()) ||
|
||
c.zip.toLowerCase().includes(search.toLowerCase()) ||
|
||
c.city.toLowerCase().includes(search.toLowerCase()) ||
|
||
c.phoneNumbers?.[0]?.number?.toLowerCase().includes(search.toLowerCase())
|
||
);
|
||
}, [customers, search]);
|
||
|
||
const paginated = useMemo(() => {
|
||
const start = (page - 1) * pageSize;
|
||
return filtered.slice(start, start + pageSize);
|
||
}, [filtered, page]);
|
||
|
||
const totalPages = Math.ceil(filtered.length / pageSize);
|
||
|
||
return (
|
||
<div className="p-6 space-y-6 text-sm overflow-x-auto">
|
||
<motion.div
|
||
initial={{opacity: 0}}
|
||
animate={{opacity: 1}}
|
||
transition={{duration: 0.3}}
|
||
className="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||
>
|
||
<Card>
|
||
<CardContent>
|
||
<div className="text-sm text-muted-foreground">Kunden</div>
|
||
<div className="text-3xl font-bold">{customers.length}</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent>
|
||
<div className="text-sm text-muted-foreground">Demo-Statistik</div>
|
||
<div className="text-3xl font-bold">–</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent>
|
||
<div className="text-sm text-muted-foreground">Letzte Aktivität</div>
|
||
<div className="text-3xl font-bold">–</div>
|
||
</CardContent>
|
||
</Card>
|
||
</motion.div>
|
||
|
||
<motion.div initial={{opacity: 0}} animate={{opacity: 1}} transition={{duration: 0.3}}>
|
||
<Card>
|
||
<CardContent className="space-y-6">
|
||
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
||
<Input placeholder="Suche" value={search} onChange={(e) => setSearch(e.target.value)}/>
|
||
<NewCustomerModal onCustomerCreated={loadCustomers}/>
|
||
</div>
|
||
|
||
{customers.length === 0 && loading ? (
|
||
<div className="text-center text-muted-foreground">Lade Kunden...</div>
|
||
) : (
|
||
<div className="overflow-x-auto">
|
||
<Table className="text-xs min-w-[700px]">
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>Name</TableHead>
|
||
<TableHead>E-Mail</TableHead>
|
||
<TableHead>Firma</TableHead>
|
||
<TableHead>Telefon</TableHead>
|
||
<TableHead>Straße</TableHead>
|
||
<TableHead>PLZ</TableHead>
|
||
<TableHead>Ort</TableHead>
|
||
<TableHead>Erstellt am</TableHead>
|
||
<TableHead>Aktionen</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{paginated.map((customer) => (
|
||
<TableRow key={customer.id}>
|
||
<TableCell
|
||
className="max-w-[180px] truncate">{customer.name}</TableCell>
|
||
<TableCell
|
||
className="max-w-[180px] truncate">{customer.email}</TableCell>
|
||
<TableCell
|
||
className="max-w-[180px] truncate">{customer.companyName}</TableCell>
|
||
<TableCell className="max-w-[140px] truncate">
|
||
{customer.phoneNumbers?.[0]?.number}
|
||
</TableCell>
|
||
<TableCell
|
||
className="max-w-[180px] truncate">{customer.street}</TableCell>
|
||
<TableCell>{customer.zip}</TableCell>
|
||
<TableCell>{customer.city}</TableCell>
|
||
<TableCell>{new Date(customer.createdAt).toLocaleString()}</TableCell>
|
||
<TableCell>
|
||
<Link href={`/customers/${customer.id}`}>
|
||
<Button variant="ghost" size="icon">
|
||
<ArrowRight className="w-4 h-4"/>
|
||
</Button>
|
||
</Link>
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
)}
|
||
|
||
<Pagination className="justify-center pt-4">
|
||
<PaginationContent>
|
||
<PaginationItem>
|
||
<PaginationPrevious
|
||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||
className={page === 1 ? "pointer-events-none opacity-50" : ""}
|
||
/>
|
||
</PaginationItem>
|
||
<PaginationItem>
|
||
<PaginationNext
|
||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||
className={page === totalPages ? "pointer-events-none opacity-50" : ""}
|
||
/>
|
||
</PaginationItem>
|
||
</PaginationContent>
|
||
</Pagination>
|
||
</CardContent>
|
||
</Card>
|
||
</motion.div>
|
||
</div>
|
||
);
|
||
}
|