e70ae79d-5898-4c55-8ceb-d2c701ce08b4
This commit is contained in:
1 parent
d226dabba3
commit
5855f699f3
53 files changed
+1911
-44
No files matched your search
@@ -0,0 +1,244 @@
|
||||
'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>
|
||||
}
|
||||
Reference in new issue
Block a user