Files
AOS/src/components/aos/company-drawer.tsx
T

245 lines
11 KiB
TypeScript

'use client'
import { useFetch } from '@/hooks/use-fetch'
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { ScrollArea } from '@/components/ui/scroll-area'
import { formatEuro, formatDate } from '@/lib/format'
import { useAos } from '@/store/aos'
import {
Building2,
Globe,
Mail,
Phone,
MapPin,
FileText,
Receipt,
FolderKanban,
KeyRound,
ShieldCheck,
Clock,
} from 'lucide-react'
interface CompanyDetail {
company: {
id: string
name: string
legalForm: string | null
vatId: string | null
registerCourt: string | null
registerNumber: string | null
website: string | null
email: string | null
phone: string | null
city: string | null
zip: string | null
street: string | null
source: string | null
tags: string | null
createdAt: string
}
persons: { id: string; firstName: string; lastName: string; email: string | null; role: string | null; phone: string | null }[]
deals: { id: string; title: string; stage: string; valueCents: number; probability: number }[]
projects: { id: string; name: string; status: string; type: string }[]
invoices: { id: string; number: string; status: string; grossCents: number; issueDate: string }[]
licenses: { id: string; key: string; product: string; status: string; activations: { id: string }[] }[]
leads: { id: string; companyName: string; score: number; status: string }[]
timeline: { kind: string; date: string; title: string; meta: string; valueCents: number | null }[]
consent: {
legalBasis: string
purpose: string
source: string
recordedAt: string
evidence: string
}
}
export function CompanyDrawer() {
const { selectedCompanyId, setSelectedCompany } = useAos()
const { data, loading } = useFetch<CompanyDetail>(
selectedCompanyId ? `/api/aos/crm/companies/${selectedCompanyId}` : null
)
return (
<Sheet open={!!selectedCompanyId} onOpenChange={(o) => !o && setSelectedCompany(null)}>
<SheetContent side="right" className="w-full overflow-hidden p-0 sm:max-w-2xl">
<SheetTitle className="sr-only">Firmendetails</SheetTitle>
{loading || !data ? (
<div className="space-y-4 p-6">
<Skeleton className="h-7 w-48" />
<Skeleton className="h-32 w-full" />
<Skeleton className="h-40 w-full" />
</div>
) : (
<>
<SheetHeader className="border-b border-border px-6 pb-4 pt-6">
<SheetTitle className="flex items-center gap-2 text-left">
<Building2 className="size-5 text-muted-foreground" />
<span>{data.company.name}</span>
{data.company.legalForm && (
<Badge variant="outline" className="text-[10px]">{data.company.legalForm}</Badge>
)}
</SheetTitle>
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-muted-foreground">
{data.company.website && (
<a href={`https://${data.company.website}`} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1 hover:text-foreground">
<Globe className="size-3" />{data.company.website}
</a>
)}
{data.company.email && <span className="flex items-center gap-1"><Mail className="size-3" />{data.company.email}</span>}
{data.company.phone && <span className="flex items-center gap-1"><Phone className="size-3" />{data.company.phone}</span>}
{data.company.city && <span className="flex items-center gap-1"><MapPin className="size-3" />{data.company.zip} {data.company.city}</span>}
</div>
</SheetHeader>
<ScrollArea className="h-[calc(100vh-9rem)]">
<div className="space-y-5 p-6">
{/* Legal */}
<section>
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">Stammdaten</h3>
<div className="grid grid-cols-2 gap-2 text-xs">
<Field label="USt-IdNr." value={data.company.vatId} mono />
<Field label="Registergericht" value={data.company.registerCourt} />
<Field label="HRB-Nummer" value={data.company.registerNumber} mono />
<Field label="Quelle" value={data.company.source} />
</div>
{data.company.tags && (
<div className="mt-2 flex flex-wrap gap-1">
{data.company.tags.split(',').map((t) => (
<Badge key={t} variant="secondary" className="text-[10px]">{t.trim()}</Badge>
))}
</div>
)}
</section>
{/* DSGVO consent */}
<section className="rounded-md border border-border bg-muted/20 p-3">
<div className="flex items-center gap-2">
<ShieldCheck className="size-4 text-muted-foreground" />
<h3 className="text-xs font-semibold uppercase tracking-wide">DSGVO-Konsens</h3>
</div>
<div className="mt-2 space-y-1.5 text-[11px]">
<ConsentRow label="Rechtsgrundlage" value={data.consent.legalBasis} />
<ConsentRow label="Zweck" value={data.consent.purpose} />
<ConsentRow label="Quelle" value={data.consent.source} />
<ConsentRow label="Nachweis" value={data.consent.evidence} />
<ConsentRow label="Erfasst am" value={formatDate(data.consent.recordedAt)} />
</div>
</section>
{/* Persons */}
<section>
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Ansprechpartner ({data.persons.length})
</h3>
<div className="space-y-1.5">
{data.persons.map((p) => (
<div key={p.id} className="flex items-center justify-between rounded-md border border-border p-2 text-xs">
<div>
<p className="font-medium">{p.firstName} {p.lastName}</p>
<p className="text-[10px] text-muted-foreground">{p.role ?? '—'}</p>
</div>
<div className="text-right text-[10px] text-muted-foreground">
{p.email && <p>{p.email}</p>}
</div>
</div>
))}
{data.persons.length === 0 && <Empty>No persons.</Empty>}
</div>
</section>
{/* Timeline */}
<section>
<h3 className="mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
<Clock className="size-3.5" />
Timeline
</h3>
<div className="space-y-1.5">
{data.timeline.map((t, i) => {
const Icon = t.kind === 'deal' ? FileText : t.kind === 'invoice' ? Receipt : t.kind === 'project' ? FolderKanban : KeyRound
return (
<div key={i} className="flex items-start gap-2.5 rounded-md border border-border p-2">
<Icon className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<p className="truncate text-xs font-medium">{t.title}</p>
{t.valueCents ? <span className="shrink-0 text-[10px] tabular-nums">{formatEuro(t.valueCents)}</span> : null}
</div>
<p className="text-[10px] text-muted-foreground">
{formatDate(t.date)} · <Badge variant="outline" className="ml-0.5 text-[9px] capitalize">{t.meta}</Badge>
</p>
</div>
</div>
)
})}
{data.timeline.length === 0 && <Empty>Noch keine Aktivität.</Empty>}
</div>
</section>
{/* Licenses + Invoices quick links */}
<section className="grid grid-cols-2 gap-3">
<div>
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">Lizenzen</h3>
<div className="space-y-1.5">
{data.licenses.map((l) => (
<div key={l.id} className="rounded-md border border-border p-2">
<p className="text-[11px] font-medium">{l.product}</p>
<code className="text-[9px] font-mono text-muted-foreground">{l.key}</code>
</div>
))}
{data.licenses.length === 0 && <Empty>Keine Lizenzen.</Empty>}
</div>
</div>
<div>
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">Rechnungen</h3>
<div className="space-y-1.5">
{data.invoices.slice(0, 5).map((i) => (
<div key={i.id} className="rounded-md border border-border p-2">
<div className="flex items-center justify-between">
<code className="text-[10px] font-mono">{i.number}</code>
<Badge variant="outline" className="text-[9px]">{i.status}</Badge>
</div>
<p className="mt-0.5 text-[10px] tabular-nums text-muted-foreground">{formatEuro(i.grossCents)}</p>
</div>
))}
{data.invoices.length === 0 && <Empty>Keine Rechnungen.</Empty>}
</div>
</div>
</section>
</div>
</ScrollArea>
</>
)}
</SheetContent>
</Sheet>
)
}
function Field({ label, value, mono }: { label: string; value?: string | null; mono?: boolean }) {
return (
<div className="rounded-md border border-border p-2">
<p className="text-[9px] uppercase tracking-wide text-muted-foreground">{label}</p>
<p className={`mt-0.5 font-medium ${mono ? 'font-mono text-[11px]' : 'text-xs'}`}>{value ?? '—'}</p>
</div>
)
}
function ConsentRow({ label, value }: { label: string; value: string }) {
return (
<div className="flex gap-2">
<span className="shrink-0 text-muted-foreground">{label}:</span>
<span className="font-medium">{value}</span>
</div>
)
}
function Empty({ children }: { children: React.ReactNode }) {
return <p className="rounded border border-dashed border-border py-3 text-center text-[10px] text-muted-foreground">{children}</p>
}