'use client' import { useState } from 'react' import { FileText, Plus, Check, X, ArrowRight, Loader2, TrendingUp, Percent, } from 'lucide-react' import { useFetch } from '@/hooks/use-fetch' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { Separator } from '@/components/ui/separator' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { PageHeader } from '../primitives' import { formatEuro, formatEuroCompact, formatDate, daysUntil } from '@/lib/format' import { cn } from '@/lib/utils' import { toast } from 'sonner' interface EstimateLine { id: string; description: string; quantity: number; unitCents: number; totalCents: number } interface Estimate { id: string number: string companyId: string companyName: string subject: string status: string validUntil: string | null issueDate: string acceptedAt: string | null acceptedBy: string | null notes: string | null netCents: number taxBp: number grossCents: number lines: EstimateLine[] projectId?: string | null } interface EstimatesData { estimates: Estimate[] stats: { total: number draft: number sent: number accepted: number rejected: number acceptedValueCents: number pendingValueCents: number winRate: number } } const STATUS_META: Record = { draft: { label: 'Entwurf', cls: 'bg-muted text-foreground' }, sent: { label: 'Versendet', cls: 'bg-foreground/50 text-background' }, accepted: { label: 'Akzeptiert', cls: 'bg-foreground text-background' }, rejected: { label: 'Abgelehnt', cls: 'bg-muted text-muted-foreground' }, expired: { label: 'Abgelaufen', cls: 'bg-muted text-muted-foreground' }, } export function EstimatesView() { const { data, loading, refetch } = useFetch('/api/aos/estimates') const [selected, setSelected] = useState(null) const [dialogOpen, setDialogOpen] = useState(false) const [action, setAction] = useState<'accept' | 'convert' | null>(null) const [busy, setBusy] = useState(false) if (loading || !data) { return (
) } const { estimates, stats } = data const openDetail = (e: Estimate) => { setSelected(e) setDialogOpen(true) } const accept = async (estimate: Estimate) => { setBusy(true) const t = toast.loading('Angebot wird akzeptiert (Projekt wird angelegt)…') try { const r = await fetch(`/api/aos/estimates/${estimate.id}/accept`, { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ acceptedBy: estimate.companyName + ' (Portal)' }), }) if (!r.ok) { const err = await r.json().catch(() => ({})) throw new Error(err.error || 'accept failed') } const data = await r.json() toast.success(`Akzeptiert — Projekt angelegt`, { id: t }) setDialogOpen(false) refetch() } catch (err) { toast.error(err instanceof Error ? err.message : 'Akzeptanz fehlgeschlagen', { id: t }) } finally { setBusy(false) } } const convert = async (estimate: Estimate) => { setBusy(true) const t = toast.loading('Rechnung wird aus Angebot erstellt…') try { const r = await fetch(`/api/aos/estimates/${estimate.id}/convert`, { method: 'POST' }) if (!r.ok) { const err = await r.json().catch(() => ({})) throw new Error(err.error || 'convert failed') } const data = await r.json() toast.success(`Rechnung ${data.invoice.number} erstellt (${formatEuro(data.invoice.grossCents)})`, { id: t }) setDialogOpen(false) refetch() } catch (err) { toast.error(err instanceof Error ? err.message : 'Umwandlung fehlgeschlagen', { id: t }) } finally { setBusy(false) } } return (
Angebot } />

Offen (versendet)

{stats.sent}

{formatEuroCompact(stats.pendingValueCents)} Volumen

Akzeptiert

{stats.accepted}

{formatEuroCompact(stats.acceptedValueCents)} Volumen

Win-Rate

{stats.winRate}%

Gesamt

{stats.total}

Angebote ({estimates.length})
{estimates.map((e) => { const st = STATUS_META[e.status] ?? STATUS_META.draft const valid = e.validUntil ? daysUntil(e.validUntil) : null const expiring = valid !== null && valid < 0 && e.status === 'sent' return ( openDetail(e)}> ) })}
Nr. Betreff Kunde Datum Gültig bis Brutto Status Aktion
{e.number} {e.subject} {e.companyName} {formatDate(e.issueDate)} {e.validUntil ? ( {formatDate(e.validUntil)} {valid !== null && e.status === 'sent' && ( {valid < 0 ? `(${Math.abs(valid)} d abgelaufen)` : `(${valid} d)`} )} ) : '—'} {formatEuro(e.grossCents)} {st.label} e2.stopPropagation()}> {e.status === 'sent' ? ( ) : e.status === 'accepted' ? ( ) : ( — )}
{/* Detail dialog */} {selected && (() => { const st = STATUS_META[selected.status] ?? STATUS_META.draft return ( <>
{selected.number} {selected.subject}
{st.label}
{/* Meta */}
{selected.acceptedAt && } {selected.acceptedBy && }
{/* Lines */}
Positionen ({selected.lines.length})
{selected.lines.map((l) => (

{l.description}

{l.quantity} × {formatEuro(l.unitCents)}

{formatEuro(l.totalCents)}

))}
Netto {formatEuro(selected.netCents)}
USt ({(selected.taxBp / 100).toFixed(0)} %) {formatEuro(selected.grossCents - selected.netCents)}
Brutto {formatEuro(selected.grossCents)}
{selected.notes && (

Notizen

{selected.notes}

)} {/* Chain indicator */}

Lead → Deal → Angebot → Projekt → Rechnung

{selected.status === 'accepted' && selected.projectId ? '✓ Akzeptiert — Projekt wurde aus diesem Angebot angelegt.' : selected.status === 'sent' ? 'Wartet auf Kunden-Akzeptanz via Portal.' : selected.status === 'rejected' ? 'Abgelehnt — ggf. neues, reduziertes Angebot erstellen.' : 'Entwurf — noch nicht versendet.'}

{selected.status === 'sent' && ( )} {selected.status === 'accepted' && ( )} ) })()}
) } function Meta({ label, value }: { label: string; value: string }) { return (

{label}

{value}

) }