feat(customers): add pagination with 5 items per page
This commit is contained in:
@@ -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 "{searchTerm}" 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user