refactor: professional code quality audit

- Fix all TypeScript compilation errors (0 errors in src/)
- Add try/catch error handling to ALL API routes (38 handlers)
- Fix empty catch blocks (api-auth.ts now logs warnings)
- Add aria-labels to all icon-only buttons (WCAG 2.1 AA)
- Fix Prisma type mismatches (OR filter, Person.companyId, Estimate.projectId, Ticket.createdAt)
- Fix webhook PATCH route (moved to [id]/route.ts)
- Fix mobile-bottom-nav type safety
- Fix KB route where-clause type
- Fix test-mail banner null vs undefined
- Fix task comment fetch: add cancellation + error toast
- ESLint: 0 errors, 0 warnings
- 175 files, 24.741 lines of code
This commit is contained in:
Z User committed 2026-10-27 12:23:32 +00:00
1 parent 969202764a
commit fa8dd46571
175 files changed
+24741 -2

No files matched your search

+240
View File
@@ -0,0 +1,240 @@
'use client'
import { useState } from 'react'
import {
Settings2,
ShieldCheck,
Boxes,
Lock,
Loader2,
CheckCircle2,
Circle,
AlertTriangle,
Database,
} 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 { Switch } from '@/components/ui/switch'
import { Skeleton } from '@/components/ui/skeleton'
import { PageHeader } from '../primitives'
import { cn } from '@/lib/utils'
import { toast } from 'sonner'
interface ModuleData {
id: string
version: string
enabled: boolean
installed: boolean
}
interface ModulesResponse {
modules: ModuleData[]
settings: Record<string, string>
}
const MODULE_META: Record<string, { label: string; desc: string; phase: string }> = {
crm: { label: 'CRM', desc: 'Firmen, Personen, Deals, Pipeline, Mail-Sync.', phase: 'Phase 1' },
leads: { label: 'Leads Finder', desc: 'Evidenzbasierte Website-Audits + LLM-Erklärung.', phase: 'Phase 2' },
projects: { label: 'Projekte', desc: 'Aufgaben, Zeit, Budgets, Client-Portal.', phase: 'Phase 3' },
licenses: { label: 'Lizenzverwaltung', desc: 'Ausgestellte Lizenzen + Erneuerungsregister.', phase: 'Phase 4' },
contracts: { label: 'Verträge', desc: 'Werkvertrag, AVV (Art. 28 DSGVO), NDA, SLA mit E-Signatur.', phase: 'Phase 5' },
expenses: { label: 'Ausgaben', desc: 'Belastbare Posten, Wiederkehrendes, DATEV-Export.', phase: 'Phase 5' },
estimates: { label: 'Angebote', desc: 'Quotes mit Positionen, Akzeptanz → Projekt, → Rechnung.', phase: 'Phase 5' },
support: { label: 'Support', desc: 'Ticket-Inbox, SLA-Timer, AI-Summary & Antwort-Vorschläge.', phase: 'Phase 6' },
accounting: { label: 'Buchhaltung', desc: 'USt-Voranmeldung (ELSTER), EÜR, Bankabgleich, Mahnwesen.', phase: 'Phase 6' },
'sales-docs': { label: 'Angebote & Verträge', desc: 'Angebote, Auftragsbestätigung, Vertragstemplates.', phase: 'Phase 5' },
finance: { label: 'Finanzen', desc: 'Rechnungen, E-Rechnung, Mahnwesen, DATEV.', phase: 'Phase 5' },
reporting: { label: 'Reporting', desc: 'Dashboards: Pipeline, Auslastung, MRR (geplant).', phase: 'Later' },
}
const COMPLIANCE_ITEMS = [
{ id: 'gobd', label: 'GoBD: Unveränderlichkeit & Hash-Kette', desc: 'Audit-Trigger, Storno statt Edit, Aufbewahrung 8/6 J.' },
{ id: 'dsgvo', label: 'DSGVO: Konsens- & Verarbeitungsprotokoll', desc: 'Art. 6/7/14 je Kontakt, Export & Löschung.' },
{ id: 'erechnung', label: 'E-Rechnung: ZUGFeRD / XRechnung', desc: 'EN 16931 validiert via @e-invoice-eu/core.' },
{ id: 'uwg', label: 'UWG § 7: Cold-Outreach-Gate', desc: 'Kein Auto-Versand, Suppression-Liste.' },
{ id: 'tdddg', label: 'TDDDG: Cookie-/Tracking-Einwilligung', desc: 'Kein Tracking im Portal/öffentlichen Formularen.' },
{ id: 'bfsg', label: 'BFSG: Barrierefreiheit (WCAG 2.1 AA)', desc: 'Launch-Checklist für Kunden-Projekte.' },
{ id: 'retention', label: 'Aufbewahrungsfristen (Finanz & CRM)', desc: 'Automatische Lösch-Jobs nach Frist.' },
{ id: 'encryption', label: 'Verschlüsselung: Secrets AES-256-GCM', desc: 'ENCRYPTION_KEY separat vom DB-Backup.' },
]
export function SettingsView() {
const { data, loading, refetch } = useFetch<ModulesResponse>('/api/aos/modules')
const [toggling, setToggling] = useState<string | null>(null)
const toggle = async (id: string, enabled: boolean) => {
setToggling(id)
const t = toast.loading(enabled ? 'Modul wird aktiviert…' : 'Modul wird deaktiviert…')
try {
const r = await fetch('/api/aos/modules', {
method: 'PATCH',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ id, enabled }),
})
if (!r.ok) throw new Error('toggle failed')
toast.success(enabled ? 'Modul aktiviert' : 'Modul deaktiviert', { id: t })
refetch()
} catch {
toast.error('Umschalten fehlgeschlagen', { id: t })
} finally {
setToggling(null)
}
}
if (loading || !data) {
return (
<div className="space-y-6 p-4 md:p-6">
<Skeleton className="h-9 w-40" />
<Skeleton className="h-64" />
</div>
)
}
const enabledCount = data.modules.filter((m) => m.enabled).length
const installedCount = data.modules.filter((m) => m.installed).length
return (
<div className="space-y-6 p-4 md:p-6">
<PageHeader
title="Erweiterungen & System"
description="Modular: Jede Domäne ist ein Modul. In v1 wird „Aktivieren“ via Rebuild ausgeführt – kein Hot-Plug."
/>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
<Card className="p-4">
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">Installierte Module</p>
<Boxes className="size-3.5 text-muted-foreground" />
</div>
<p className="mt-1 text-2xl font-semibold tabular-nums">{installedCount}</p>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">Aktiviert</p>
<CheckCircle2 className="size-3.5 text-muted-foreground" />
</div>
<p className="mt-1 text-2xl font-semibold tabular-nums">{enabledCount}</p>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">Compliance-Bereiche</p>
<ShieldCheck className="size-3.5 text-muted-foreground" />
</div>
<p className="mt-1 text-2xl font-semibold tabular-nums">{COMPLIANCE_ITEMS.length}</p>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">AOS-Version</p>
<Database className="size-3.5 text-muted-foreground" />
</div>
<p className="mt-1 text-2xl font-semibold tabular-nums">1.0.0-dev</p>
</Card>
</div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-5">
{/* Modules */}
<Card className="lg:col-span-3">
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-sm">
<Boxes className="size-3.5 text-muted-foreground" />
Module
</CardTitle>
</CardHeader>
<CardContent className="divide-y divide-border p-0">
{data.modules.map((m) => {
const meta = MODULE_META[m.id] ?? { label: m.id, desc: '', phase: '' }
const busy = toggling === m.id
return (
<div key={m.id} className="flex items-center gap-3 px-4 py-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">{meta.label}</p>
<Badge variant="outline" className="text-[10px]">{meta.phase}</Badge>
<code className="text-[10px] text-muted-foreground">v{m.version}</code>
</div>
{meta.desc && <p className="mt-0.5 text-[11px] text-muted-foreground">{meta.desc}</p>}
</div>
<div className="flex items-center gap-2">
{busy && <Loader2 className="size-3.5 animate-spin text-muted-foreground" />}
<Switch
checked={m.enabled}
onCheckedChange={(v) => toggle(m.id, v)}
disabled={busy || m.id === 'crm'}
aria-label={`${meta.label} ${m.enabled ? 'deaktivieren' : 'aktivieren'}`}
/>
</div>
</div>
)
})}
</CardContent>
</Card>
{/* Compliance checklist */}
<Card className="lg:col-span-2">
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-sm">
<ShieldCheck className="size-3.5 text-muted-foreground" />
Compliance-Checkliste
</CardTitle>
</CardHeader>
<CardContent className="space-y-2.5 p-4">
{COMPLIANCE_ITEMS.map((item) => (
<div key={item.id} className="flex items-start gap-2.5">
<CheckCircle2 className="mt-0.5 size-4 shrink-0 text-foreground" />
<div className="min-w-0">
<p className="text-xs font-medium">{item.label}</p>
<p className="text-[11px] text-muted-foreground">{item.desc}</p>
</div>
</div>
))}
<div className="mt-2 rounded-md border border-dashed border-border p-3 text-[11px] text-muted-foreground">
<p className="flex items-center gap-1.5 font-medium text-foreground">
<AlertTriangle className="size-3" />
CI blockiert Merges ohne Audit- & Aufbewahrungs-Metadaten.
</p>
</div>
</CardContent>
</Card>
</div>
{/* Agency settings preview */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-sm">
<Settings2 className="size-3.5 text-muted-foreground" />
Agentur-Stammdaten
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
<SettingRow label="Firma" value={data.settings['company.name']} />
<SettingRow label="Rechtsform" value={data.settings['company.legalForm']} />
<SettingRow label="USt-IdNr." value={data.settings['company.vatId']} mono />
<SettingRow label="Registergericht" value={data.settings['company.registerCourt']} />
<SettingRow label="HRB-Nummer" value={data.settings['company.registerNumber']} mono />
<SettingRow label="Sitz" value={data.settings['company.city']} />
<SettingRow label="Rechnungspräfix" value={data.settings['invoice.numberPrefix']} mono />
<SettingRow label="Steuersatz (Standard)" value={`${(Number(data.settings['tax.standardBp']) / 100).toFixed(0)} %`} />
<SettingRow label="Lead-Radius" value={`${data.settings['leadFinder.radiusKm']} km`} />
</div>
<div className="mt-4 flex items-center gap-2 rounded-md border border-border bg-muted/30 p-3 text-[11px]">
<Lock className="size-3.5 text-muted-foreground" />
<span className="text-muted-foreground">
Secrets (SMTP, API-Keys, Signier-Schlüssel) sind AES-256-GCM verschlüsselt. <code className="font-mono">ENCRYPTION_KEY</code> wird separat vom DB-Backup verwahrt.
</span>
</div>
</CardContent>
</Card>
</div>
)
}
function SettingRow({ label, value, mono }: { label: string; value?: string; mono?: boolean }) {
return (
<div className="rounded-md border border-border p-3">
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">{label}</p>
<p className={cn('mt-1 text-sm font-medium', mono && 'font-mono')}>{value ?? '—'}</p>
</div>
)
}