'use client' import { useState, useEffect, useMemo, useCallback, Suspense } from 'react' import { useLocale, useTranslations } from 'next-intl' import { useSearchParams, useRouter, usePathname } from 'next/navigation' import { createClient } from '@/lib/supabase/client' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import { Skeleton } from '@/components/ui/skeleton' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { useToast } from '@/components/ui/use-toast' import { getErrorMessage, type ErrorLocale } from '@/lib/errors/get-error-message' import { Plus, Search, Users, Lock, ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react' import CustomerForm from '@/components/customers/CustomerForm' import { EmptyCustomers, EmptyState } from '@/components/ui/empty-state' import { PageHeader } from '@/components/ui/page-header' import { ReportExportMenu } from '@/components/reports/ReportExportMenu' import { formatDate } from '@/lib/utils' import Link from 'next/link' import { useCompany } from '@/contexts/CompanyContext' import { useCanWrite } from '@/lib/hooks/use-can-write' import type { Customer, CustomerType, CreateCustomerInput } from '@/types' const CUSTOMER_TYPE_LABEL_KEYS: Record = { individual: 'type_individual', swedish_business: 'type_swedish_business', eu_business: 'type_eu_business', non_eu_business: 'type_non_eu_business', } type SortColumn = 'name' | 'customer_type' | 'identifier' | 'email' | 'city' | 'created_at' type SortDir = 'asc' | 'desc' const SORTABLE_COLUMNS: ReadonlyArray = [ 'name', 'customer_type', 'identifier', 'email', 'city', 'created_at', ] function getInitials(name: string): string { return name .split(' ') .map((w) => w[0]) .slice(0, 2) .join('') .toUpperCase() } function getIdentifier(customer: Customer): string { return customer.org_number || customer.personal_number || '' } function compareStrings(a: string, b: string): number { return a.localeCompare(b, 'sv', { sensitivity: 'base' }) } function CustomersPageInner() { const { company } = useCompany() const { canWrite } = useCanWrite() const [customers, setCustomers] = useState([]) const [isLoading, setIsLoading] = useState(true) const [searchTerm, setSearchTerm] = useState('') const [isDialogOpen, setIsDialogOpen] = useState(false) const [isCreating, setIsCreating] = useState(false) const { toast } = useToast() const supabase = createClient() const t = useTranslations('customers') const errorLocale = useLocale() as ErrorLocale const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() const sortParam = searchParams.get('sort') const dirParam = searchParams.get('dir') const sortColumn: SortColumn = (SORTABLE_COLUMNS as ReadonlyArray).includes(sortParam ?? '') ? (sortParam as SortColumn) : 'name' const sortDir: SortDir = dirParam === 'desc' ? 'desc' : 'asc' const updateSort = useCallback( (column: SortColumn) => { const params = new URLSearchParams(searchParams.toString()) let nextDir: SortDir = 'asc' if (column === sortColumn) { nextDir = sortDir === 'asc' ? 'desc' : 'asc' } params.set('sort', column) params.set('dir', nextDir) router.replace(`${pathname}?${params.toString()}`, { scroll: false }) }, [searchParams, sortColumn, sortDir, router, pathname] ) async function fetchCustomers() { if (!company) return setIsLoading(true) const { data, error } = await supabase .from('customers') .select('*') .eq('company_id', company.id) .order('name', { ascending: true }) if (error) { toast({ title: t('load_failed_title'), description: t('load_failed_description'), variant: 'destructive', }) } else { setCustomers(data || []) } setIsLoading(false) } useEffect(() => { fetchCustomers() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) async function handleCreateCustomer(data: CreateCustomerInput) { setIsCreating(true) const response = await fetch('/api/customers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) const result = await response.json() if (!response.ok) { toast({ title: t('create_failed_title'), description: getErrorMessage(result, { context: 'customer', locale: errorLocale }), variant: 'destructive', }) } else { toast({ title: t('created_title'), description: t('created_description', { name: data.name }), }) setCustomers([...customers, result.data]) setIsDialogOpen(false) } setIsCreating(false) } const filteredCustomers = useMemo(() => { const term = searchTerm.trim().toLowerCase() if (!term) return customers return customers.filter((c) => { return ( c.name.toLowerCase().includes(term) || c.email?.toLowerCase().includes(term) || c.org_number?.includes(term) || c.personal_number?.includes(term) || c.city?.toLowerCase().includes(term) || c.notes?.toLowerCase().includes(term) ) }) }, [customers, searchTerm]) const sortedCustomers = useMemo(() => { const arr = [...filteredCustomers] arr.sort((a, b) => { let av = '' let bv = '' switch (sortColumn) { case 'name': av = a.name || '' bv = b.name || '' break case 'customer_type': av = a.customer_type || '' bv = b.customer_type || '' break case 'identifier': av = getIdentifier(a) bv = getIdentifier(b) break case 'email': av = a.email || '' bv = b.email || '' break case 'city': av = a.city || '' bv = b.city || '' break case 'created_at': av = a.created_at || '' bv = b.created_at || '' break } const cmp = compareStrings(av, bv) return sortDir === 'asc' ? cmp : -cmp }) return arr }, [filteredCustomers, sortColumn, sortDir]) function SortableHeader({ column, label, className, }: { column: SortColumn label: string className?: string }) { const isActive = sortColumn === column const Icon = isActive ? (sortDir === 'asc' ? ChevronUp : ChevronDown) : ChevronsUpDown return ( ) } return (
{t('add_customer')}
} /> {/* Search */}
setSearchTerm(e.target.value)} className="pl-10" />
{/* Customer list */} {isLoading ? ( <> {/* Desktop skeleton */} {[1, 2, 3, 4, 5].map((i) => ( ))} {/* Mobile skeleton */}
{[1, 2, 3].map((i) => ( ))}
) : sortedCustomers.length === 0 ? ( {searchTerm ? ( ) : ( setIsDialogOpen(true)} /> )} ) : ( <> {/* Desktop table */} {sortedCustomers.map((customer) => { const identifier = getIdentifier(customer) return ( router.push(`/customers/${customer.id}`)} > e.stopPropagation()} > {customer.name} {t(CUSTOMER_TYPE_LABEL_KEYS[customer.customer_type])}
{identifier || '—'} {customer.org_number && customer.vat_number_validated && ( {t('verified')} )}
{customer.email || '—'} {customer.city || '—'} {formatDate(customer.created_at)}
) })}
{/* Mobile card list */}
{sortedCustomers.map((customer) => (
{getInitials(customer.name)}
{customer.name} {t(CUSTOMER_TYPE_LABEL_KEYS[customer.customer_type])}
{customer.email &&

{customer.email}

} {getIdentifier(customer) && (
{getIdentifier(customer)} {customer.org_number && customer.vat_number_validated && ( {t('verified')} )}
)} {customer.city && (

{customer.city}, {customer.country}

)}
))}
)} ) } export default function CustomersPage() { return ( ) }