Add customer management
This commit is contained in:
196
internal_frontend/app/customers/page.tsx
Normal file
196
internal_frontend/app/customers/page.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
"use client";
|
||||
|
||||
import {useState, useEffect, useMemo} from "react";
|
||||
import {useRouter} from "next/navigation";
|
||||
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 axios from "axios";
|
||||
|
||||
export interface CustomerPhoneNumber {
|
||||
number: string;
|
||||
note: string;
|
||||
creator: string;
|
||||
lastModifier: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CustomerNote {
|
||||
text: string;
|
||||
creator: string;
|
||||
lastModifier: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Customer {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
companyName: string;
|
||||
phoneNumbers: CustomerPhoneNumber[];
|
||||
street: string;
|
||||
zip: string;
|
||||
city: string;
|
||||
notes: CustomerNote[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function CustomersPage() {
|
||||
const router = useRouter();
|
||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
||||
const [search, setSearch] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const pageSize = 15;
|
||||
|
||||
useEffect(() => {
|
||||
axios.get("/api/customers").then((res) => {
|
||||
setCustomers(res.data);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return customers.filter(
|
||||
(c) =>
|
||||
c.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
c.email.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>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => router.push(`/customers/${customer.id}`)}
|
||||
>
|
||||
<ArrowRight className="w-4 h-4"/>
|
||||
</Button>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user