'use client' import { useMemo, useState } from 'react' import { useTranslations } from 'next-intl' import { Loader2, Search } from 'lucide-react' import { cn, formatDate } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { TH_CLASS, TD_CLASS } from '@/components/ui/dry-table' import { useToast } from '@/components/ui/use-toast' import { performCompanySwitch } from '@/lib/company/switch-client' import { filterClientRows, type ClientOverviewRow } from '@/lib/clients/aggregate' /** * The cockpit client list (WL-14): dry-table, urgency-sorted server-side, * free-text filter client-side. Entering a client is the WL-09 soft switch: * sets the active company and lands DIRECTLY on that company's overview via * performCompanySwitch (one full-page load to the target, no start-page * bounce). The way back is the Klienter nav entry, which needs no switch. */ export default function ClientsTable({ clients }: { clients: ClientOverviewRow[] }) { const t = useTranslations('clients') const { toast } = useToast() const [query, setQuery] = useState('') const [pendingId, setPendingId] = useState(null) const rows = useMemo(() => filterClientRows(clients, query), [clients, query]) const handleEnter = async (companyId: string) => { if (pendingId) return setPendingId(companyId) const result = await performCompanySwitch(companyId, { destination: '/' }) if (result?.error) { setPendingId(null) toast({ title: t('enter_failed'), variant: 'destructive' }) } } if (clients.length === 0) { return (

{t('empty')}

) } return (
setQuery(e.target.value)} placeholder={t('filter_placeholder')} aria-label={t('filter_placeholder')} className="w-full bg-transparent text-[13px] text-foreground placeholder:text-muted-foreground/60 focus:outline-none" />
{rows.length === 0 && ( )} {rows.map((row) => ( void handleEnter(row.companyId)} className={cn( 'cursor-pointer transition-colors duration-150 hover:bg-secondary/35', pendingId && pendingId !== row.companyId && 'opacity-50', )} > ))}
{t('col_company')} {t('col_unbooked')} {t('col_inbox')} {t('col_next_deadline')} {t('col_last_booked')}
{t('no_matches')}
{row.unbookedCount > 0 ? ( row.unbookedCount ) : ( 0 )} {row.inboxCount > 0 ? ( row.inboxCount ) : ( 0 )} {row.nextDeadline ? ( {/* Chips mark exceptions: only overdue/action_needed deviate; an ordinary upcoming deadline is muted text. */} {row.nextDeadline.urgency === 'overdue' && ( {t('deadline_overdue')} )} {row.nextDeadline.urgency === 'action_needed' && ( {t('deadline_action_needed')} )} {row.nextDeadline.title} {formatDate(row.nextDeadline.dueDate)} ) : ( - )} {row.lastBookedDate ? ( {formatDate(row.lastBookedDate)} ) : ( {t('never_booked')} )}
) }