245 lines
11 KiB
TypeScript
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>
|
|
}
|