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:
1 parent
969202764a
commit
fa8dd46571
175 files changed
+24741
-2
No files matched your search
Executable
+240
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user