Files
rheinsw-mono-repo/internal_frontend/app/customers/page.tsx
Thatsaphorn Atchariyaphap 18014ce9f0 Introduce ErrorBoundary component for improved error handling
- Add `ErrorBoundary` component to handle rendering fallback UI during errors.
- Integrate `ErrorBoundary` into main layout and critical pages for global error handling.
- Create `useErrorHandler` hook to handle async errors consistently across components.
- Refactor `NewCustomerModal` and `CustomersPage` to leverage `useErrorHandler` for improved error management.
- Add a fallback dialog UI for user-friendly error reporting and resolution.
2025-07-11 19:06:07 +02:00

187 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import {useState, useEffect, useMemo} 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";
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();
useEffect(() => {
setLoading(true);
fetch('/api/customers')
.then(async (response) => {
if (!response.ok) {
throw new Error(`Failed to fetch customers: ${response.statusText}`);
}
return response.json();
})
.then((data) => {
setCustomers(data);
})
.catch((error) => {
handleError(error);
setCustomers([]);
})
.finally(() => {
setLoading(false);
});
}, [handleError]);
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/>
</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>
);
}