Files
AOS/src/components/aos/views/settings.tsx
T
Z User fa8dd46571 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
2026-10-27 12:23:32 +00:00

241 lines
11 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}