feat(customers): add pagination with 5 items per page

This commit is contained in:
DanielS
2026-09-07 23:34:41 +02:00
parent facf481fdb
commit 40b63b3fa9

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import React, { useState } from 'react' import React, { useState, useMemo } from 'react'
import { motion, AnimatePresence } from 'framer-motion' import { motion, AnimatePresence } from 'framer-motion'
import Link from 'next/link' import Link from 'next/link'
import type { EndCustomerWithDevices, FlattenedDevice } from '@/lib/types' import type { EndCustomerWithDevices, FlattenedDevice } from '@/lib/types'
@@ -17,6 +17,10 @@ import {
AlertTriangle, AlertTriangle,
Search, Search,
Package, Package,
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
} from 'lucide-react' } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
@@ -188,6 +192,8 @@ export function CustomerAccordionList({ customers }: CustomerAccordionListProps)
}) })
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
const [currentPage, setCurrentPage] = useState(1)
const pageSize = 5
const toggleCustomer = (id: string) => { const toggleCustomer = (id: string) => {
setOpenCustomerIds((prev) => ({ setOpenCustomerIds((prev) => ({
@@ -196,8 +202,11 @@ export function CustomerAccordionList({ customers }: CustomerAccordionListProps)
})) }))
} }
const filteredCustomers = customers.filter((c) => { const filteredCustomers = useMemo(() => {
const term = searchTerm.toLowerCase() const term = searchTerm.toLowerCase().trim()
if (!term) return customers
return customers.filter((c) => {
return ( return (
c.company_name.toLowerCase().includes(term) || c.company_name.toLowerCase().includes(term) ||
(c.first_name && c.first_name.toLowerCase().includes(term)) || (c.first_name && c.first_name.toLowerCase().includes(term)) ||
@@ -206,6 +215,19 @@ export function CustomerAccordionList({ customers }: CustomerAccordionListProps)
(c.email && c.email.toLowerCase().includes(term)) (c.email && c.email.toLowerCase().includes(term))
) )
}) })
}, [customers, searchTerm])
const totalPages = Math.ceil(filteredCustomers.length / pageSize) || 1
const paginatedCustomers = useMemo(() => {
const from = (currentPage - 1) * pageSize
return filteredCustomers.slice(from, from + pageSize)
}, [filteredCustomers, currentPage, pageSize])
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value)
setCurrentPage(1)
}
if (customers.length === 0) { if (customers.length === 0) {
return ( return (
@@ -225,21 +247,42 @@ export function CustomerAccordionList({ customers }: CustomerAccordionListProps)
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Suchfeld */} {/* Suchfeld & Info */}
<div className="relative max-w-md"> <div className="flex flex-col sm:flex-row gap-4 justify-between items-stretch sm:items-center">
<div className="relative max-w-md flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input <Input
type="text" type="text"
placeholder="Kunden suchen (Firma, Name, Stadt)..." placeholder="Kunden suchen (Firma, Name, Stadt)..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={handleSearchChange}
className="pl-9 bg-slate-900/50 border-white/10 text-white placeholder:text-slate-500 focus:border-primary" className="pl-9 bg-slate-900/50 border-white/10 text-white placeholder:text-slate-500 focus:border-primary"
/> />
</div> </div>
<div className="text-xs text-slate-400 bg-slate-900/50 px-3.5 py-2 rounded-xl border border-white/10 shrink-0 text-center flex items-center justify-center gap-2">
<span>Kunden:</span>
<span className="text-white font-bold">{filteredCustomers.length}</span>
{searchTerm && <span className="text-slate-500">(gefiltert aus {customers.length})</span>}
</div>
</div>
{/* Keine Suchergebnisse */}
{filteredCustomers.length === 0 && (
<Card className="glass-dark border-white/10">
<CardContent className="flex flex-col items-center justify-center py-12 gap-3 text-center">
<Search className="w-10 h-10 text-slate-600" />
<p className="text-slate-300 font-medium">Keine Kunden für &quot;{searchTerm}&quot; gefunden.</p>
<Button variant="outline" size="sm" onClick={() => setSearchTerm('')} className="border-white/10 text-xs">
Filter zurücksetzen
</Button>
</CardContent>
</Card>
)}
{/* Akkordeon-Liste */} {/* Akkordeon-Liste */}
{paginatedCustomers.length > 0 && (
<div className="space-y-4"> <div className="space-y-4">
{filteredCustomers.map((customer) => { {paginatedCustomers.map((customer) => {
const isOpen = !!openCustomerIds[customer.id] const isOpen = !!openCustomerIds[customer.id]
const devices = customer.devices || [] const devices = customer.devices || []
@@ -370,6 +413,57 @@ export function CustomerAccordionList({ customers }: CustomerAccordionListProps)
) )
})} })}
</div> </div>
)}
{/* Paginierung (5 Kunden pro Seite) */}
{totalPages > 1 && (
<div className="flex items-center justify-between pt-4 border-t border-white/10">
<span className="text-xs text-slate-400">
Seite <span className="text-white font-bold">{currentPage}</span> von{' '}
<span className="text-white font-bold">{totalPages}</span>
</span>
<div className="flex items-center gap-1.5">
<Button
variant="ghost"
size="sm"
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="h-8 px-2 text-slate-400 hover:text-white"
title="Erste Seite"
>
<ChevronsLeft className="w-4 h-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="border-white/10 text-xs gap-1 rounded-xl h-8 px-3"
>
<ChevronLeft className="w-3.5 h-3.5" /> Vorherige
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="border-white/10 text-xs gap-1 rounded-xl h-8 px-3"
>
Nächste <ChevronRight className="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages}
className="h-8 px-2 text-slate-400 hover:text-white"
title="Letzte Seite"
>
<ChevronsRight className="w-4 h-4" />
</Button>
</div>
</div>
)}
</div> </div>
) )
} }