161 lines
7.4 KiB
TypeScript
161 lines
7.4 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
import { FileText, Plus, Search, X } from 'lucide-react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useSession } from 'next-auth/react';
|
|
|
|
const typeLabels: Record<string, string> = {
|
|
QUOTE: 'Angebot', ORDER_CONFIRMATION: 'Auftragsbestätigung',
|
|
DELIVERY_NOTE: 'Lieferschein', INVOICE: 'Rechnung', CREDIT_NOTE: 'Rechnungskorrektur'
|
|
};
|
|
const typeColors: Record<string, string> = {
|
|
QUOTE: 'bg-blue-50 text-blue-700 border-blue-200',
|
|
ORDER_CONFIRMATION: 'bg-indigo-50 text-indigo-700 border-indigo-200',
|
|
DELIVERY_NOTE: 'bg-amber-50 text-amber-700 border-amber-200',
|
|
INVOICE: 'bg-emerald-50 text-emerald-700 border-emerald-200',
|
|
CREDIT_NOTE: 'bg-red-50 text-red-700 border-red-200'
|
|
};
|
|
const statusLabels: Record<string, string> = {
|
|
DRAFT: 'Entwurf', SENT: 'Gesendet', ACCEPTED: 'Angenommen',
|
|
REJECTED: 'Abgelehnt', DELIVERED: 'Geliefert', PAID: 'Bezahlt',
|
|
CANCELLED: 'Storniert', ARCHIVED: 'Archiviert'
|
|
};
|
|
const statusColors: Record<string, string> = {
|
|
DRAFT: 'bg-slate-100 text-slate-600', SENT: 'bg-blue-100 text-blue-700',
|
|
ACCEPTED: 'bg-emerald-100 text-emerald-700', REJECTED: 'bg-red-100 text-red-700',
|
|
DELIVERED: 'bg-amber-100 text-amber-700', PAID: 'bg-emerald-100 text-emerald-700',
|
|
CANCELLED: 'bg-slate-200 text-slate-500', ARCHIVED: 'bg-slate-100 text-slate-400'
|
|
};
|
|
|
|
export default function SalesPage() {
|
|
const [docs, setDocs] = useState<any[]>([]);
|
|
const [filter, setFilter] = useState('ACTIVE');
|
|
const [search, setSearch] = useState('');
|
|
const router = useRouter();
|
|
const { data: session } = useSession();
|
|
const perms = (session?.user as any)?.permissions || [];
|
|
const canCreate = perms.includes('SALES_MANAGE');
|
|
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
useEffect(() => { fetchDocs(); }, []);
|
|
|
|
const fetchDocs = async () => {
|
|
const res = await fetch('/api/sales');
|
|
if (res.ok) setDocs(await res.json());
|
|
};
|
|
|
|
// Filter + Search logic
|
|
const filtered = docs.filter(d => {
|
|
// Type filter
|
|
if (filter === 'ACTIVE' && (d.status === 'ARCHIVED' || d.status === 'CANCELLED')) return false;
|
|
if (filter === 'ARCHIVED' && d.status !== 'ARCHIVED') return false;
|
|
if (filter !== 'ALL' && filter !== 'ACTIVE' && filter !== 'ARCHIVED' && d.type !== filter) return false;
|
|
|
|
// Search
|
|
if (search.trim()) {
|
|
const q = search.toLowerCase();
|
|
const customerName = d.customer?.companyName || `${d.customer?.firstName} ${d.customer?.lastName}`;
|
|
return (
|
|
d.number.toLowerCase().includes(q) ||
|
|
customerName.toLowerCase().includes(q) ||
|
|
(typeLabels[d.type] || '').toLowerCase().includes(q) ||
|
|
(statusLabels[d.status] || '').toLowerCase().includes(q)
|
|
);
|
|
}
|
|
return true;
|
|
});
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto space-y-6 animate-fade-in-up">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
|
|
<FileText className="w-6 h-6 text-indigo-600" /> Verkauf
|
|
</h1>
|
|
<p className="text-slate-500 mt-1">Angebote, Auftragsbestätigungen, Lieferscheine & Rechnungen.</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{/* Live Search */}
|
|
<div className="relative">
|
|
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
|
|
<input type="text" placeholder="Nr., Kunde, Typ suchen..." value={search}
|
|
onChange={e => setSearch(e.target.value)}
|
|
className="pl-9 pr-8 py-2 border border-slate-300 rounded-xl text-sm outline-none focus-ring w-64" />
|
|
{search && (
|
|
<button onClick={() => setSearch('')} className="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600">
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
{canCreate && (
|
|
<button onClick={() => router.push('/sales/new')} className="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition flex items-center gap-2 font-medium shadow-sm">
|
|
<Plus className="w-4 h-4" /> Neuer Beleg
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2 flex-wrap">
|
|
{[
|
|
{ key: 'ACTIVE', label: 'Aktiv' },
|
|
{ key: 'ALL', label: 'Alle' },
|
|
{ key: 'QUOTE', label: 'Angebote' },
|
|
{ key: 'ORDER_CONFIRMATION', label: 'ABs' },
|
|
{ key: 'DELIVERY_NOTE', label: 'Lieferscheine' },
|
|
{ key: 'INVOICE', label: 'Rechnungen' },
|
|
{ key: 'CREDIT_NOTE', label: 'Korrekturen' },
|
|
{ key: 'ARCHIVED', label: 'Archiv' }
|
|
].map(f => (
|
|
<button key={f.key} onClick={() => setFilter(f.key)}
|
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition ${filter === f.key ? 'bg-indigo-600 text-white' : 'bg-white text-slate-600 border border-slate-200 hover:bg-slate-50'}`}>
|
|
{f.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="bg-white rounded-2xl shadow-lg shadow-slate-200/50 border border-slate-100 overflow-hidden">
|
|
<table className="w-full text-sm text-left">
|
|
<thead className="bg-slate-50 text-slate-600 font-medium border-b border-slate-200">
|
|
<tr>
|
|
<th className="py-4 px-6">Nummer</th>
|
|
<th className="py-4 px-6">Typ</th>
|
|
<th className="py-4 px-6">Kunde</th>
|
|
<th className="py-4 px-6">Status</th>
|
|
<th className="py-4 px-6 text-right">Betrag</th>
|
|
<th className="py-4 px-6">Datum</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{filtered.map(d => (
|
|
<tr key={d.id} onClick={() => router.push(`/sales/${d.id}`)}
|
|
className={`hover:bg-indigo-50/40 transition-colors cursor-pointer ${d.status === 'ARCHIVED' ? 'opacity-60' : ''}`}>
|
|
<td className="py-4 px-6 font-mono font-semibold text-slate-900">{d.number}</td>
|
|
<td className="py-4 px-6">
|
|
<span className={`px-2.5 py-1 rounded-md text-xs font-semibold border ${typeColors[d.type]}`}>{typeLabels[d.type]}</span>
|
|
</td>
|
|
<td className="py-4 px-6 text-slate-700">{d.customer?.companyName || `${d.customer?.firstName} ${d.customer?.lastName}`}</td>
|
|
<td className="py-4 px-6">
|
|
<span className={`px-2.5 py-1 rounded-full text-xs font-bold ${statusColors[d.status]}`}>{statusLabels[d.status]}</span>
|
|
</td>
|
|
<td className="py-4 px-6 text-right font-semibold text-slate-900">{d.total.toFixed(2)} €</td>
|
|
<td className="py-4 px-6 text-slate-500">{new Date(d.createdAt).toLocaleDateString('de-DE')}</td>
|
|
</tr>
|
|
))}
|
|
{filtered.length === 0 && (
|
|
<tr><td colSpan={6} className="py-8 text-center text-slate-500">
|
|
{search ? `Keine Ergebnisse für "${search}"` : 'Keine Belege vorhanden.'}
|
|
</td></tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
{filtered.length > 0 && (
|
|
<div className="px-6 py-3 bg-slate-50 border-t border-slate-100 text-xs text-slate-500">
|
|
{filtered.length} Beleg{filtered.length !== 1 ? 'e' : ''} angezeigt
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|