0ca22831-efa9-41b6-87bd-e5ee6c344547
This commit is contained in:
1 parent
468e7ad123
commit
09ca75d36e
9 files changed
+294
-1
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
@@ -27,6 +27,7 @@ import { SettingsView } from './views/settings'
|
|||||||
import { GlobalSettingsView } from './views/global-settings'
|
import { GlobalSettingsView } from './views/global-settings'
|
||||||
import { SetupWizardView } from './views/setup-wizard'
|
import { SetupWizardView } from './views/setup-wizard'
|
||||||
import { PortalShell } from './portal-shell'
|
import { PortalShell } from './portal-shell'
|
||||||
|
import { OnboardingOverlay } from './onboarding-overlay'
|
||||||
|
|
||||||
export function AppShell() {
|
export function AppShell() {
|
||||||
const { view, sidebarOpen, setSidebarOpen, portalMode } = useAos()
|
const { view, sidebarOpen, setSidebarOpen, portalMode } = useAos()
|
||||||
@@ -80,6 +81,7 @@ export function AppShell() {
|
|||||||
|
|
||||||
<CommandMenu />
|
<CommandMenu />
|
||||||
<CompanyDrawer />
|
<CompanyDrawer />
|
||||||
|
<OnboardingOverlay />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useAos } from '@/store/aos'
|
||||||
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Progress } from '@/components/ui/progress'
|
||||||
|
import { Check, ChevronRight, Rocket } from 'lucide-react'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useI18n } from '@/hooks/use-i18n'
|
||||||
|
|
||||||
|
const CHECKLIST_ITEMS = [
|
||||||
|
{ key: 'visit_leads', view: 'leads', label_de: 'Leads Finder erkunden', label_en: 'Explore Leads Finder', label_tr: 'Leads Finder keşfet' },
|
||||||
|
{ key: 'visit_projects', view: 'projects', label_de: 'Projekte ansehen', label_en: 'View Projects', label_tr: 'Projeleri görüntüle' },
|
||||||
|
{ key: 'visit_finance', view: 'finance', label_de: 'Rechnungen prüfen', label_en: 'Check Invoices', label_tr: 'Faturaları kontrol et' },
|
||||||
|
{ key: 'visit_accounting', view: 'accounting', label_de: 'Buchhaltung (UStVA) ansehen', label_en: 'View Accounting (VAT)', label_tr: 'Muhasebeyi (KDV) görüntüle' },
|
||||||
|
{ key: 'visit_api', view: 'api', label_de: 'API-Token erstellen', label_en: 'Create API Token', label_tr: 'API Token oluştur' },
|
||||||
|
{ key: 'visit_portal', view: 'dashboard', label_de: 'Client-Portal öffnen', label_en: 'Open Client Portal', label_tr: 'Müşteri Portalını aç' },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export function OnboardingChecklist() {
|
||||||
|
const { checklist, toggleChecklist, setView, setPortalMode } = useAos()
|
||||||
|
const { locale } = useI18n()
|
||||||
|
|
||||||
|
const completed = CHECKLIST_ITEMS.filter((item) => checklist[item.key]).length
|
||||||
|
const total = CHECKLIST_ITEMS.length
|
||||||
|
const pct = Math.round((completed / total) * 100)
|
||||||
|
const allDone = completed === total
|
||||||
|
|
||||||
|
const label = (item: typeof CHECKLIST_ITEMS[number]) => {
|
||||||
|
if (locale === 'en') return item.label_en
|
||||||
|
if (locale === 'tr') return item.label_tr
|
||||||
|
return item.label_de
|
||||||
|
}
|
||||||
|
|
||||||
|
const go = (item: typeof CHECKLIST_ITEMS[number]) => {
|
||||||
|
if (item.view === 'dashboard' && item.key === 'visit_portal') {
|
||||||
|
setPortalMode(true)
|
||||||
|
} else {
|
||||||
|
setView(item.view as 'leads' | 'projects' | 'finance' | 'accounting' | 'api' | 'dashboard')
|
||||||
|
}
|
||||||
|
if (!checklist[item.key]) toggleChecklist(item.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allDone) {
|
||||||
|
return (
|
||||||
|
<Card className="border-foreground/30 bg-foreground/5">
|
||||||
|
<CardContent className="flex items-center gap-3 p-4">
|
||||||
|
<div className="flex size-10 items-center justify-center rounded-full bg-foreground text-background">
|
||||||
|
<Check className="size-5" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-semibold">Onboarding abgeschlossen!</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Du kennst nun alle Kernfunktionen. Bei Fragen sieh im Handbuch (Wissensdatenbank) nach.</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setView('kb')}>Zum Handbuch</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Rocket className="size-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-semibold">Onboarding-Checkliste</span>
|
||||||
|
<Badge variant="outline" className="text-[10px]">{completed}/{total}</Badge>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground">{pct}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={pct} className="mb-3 h-1.5" />
|
||||||
|
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{CHECKLIST_ITEMS.map((item) => {
|
||||||
|
const done = checklist[item.key]
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.key}
|
||||||
|
onClick={() => go(item)}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-md border p-2 text-left transition hover:bg-muted/50',
|
||||||
|
done ? 'border-foreground/30 bg-foreground/5' : 'border-border'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={cn(
|
||||||
|
'flex size-5 shrink-0 items-center justify-center rounded border',
|
||||||
|
done ? 'border-foreground bg-foreground text-background' : 'border-muted-foreground/40'
|
||||||
|
)}>
|
||||||
|
{done && <Check className="size-3" />}
|
||||||
|
</div>
|
||||||
|
<span className={cn('flex-1 text-xs', done && 'text-muted-foreground line-through')}>{label(item)}</span>
|
||||||
|
{!done && <ChevronRight className="size-3 shrink-0 text-muted-foreground" />}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useAos } from '@/store/aos'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import {
|
||||||
|
Rocket,
|
||||||
|
Radar,
|
||||||
|
FolderKanban,
|
||||||
|
Receipt,
|
||||||
|
BookOpen,
|
||||||
|
Webhook,
|
||||||
|
Check,
|
||||||
|
X,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronLeft,
|
||||||
|
Sparkles,
|
||||||
|
ShieldCheck,
|
||||||
|
CalendarDays,
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
const STEPS = [
|
||||||
|
{
|
||||||
|
icon: Rocket,
|
||||||
|
title: 'Willkommen bei AOS',
|
||||||
|
description: 'Die selbstgehostete Plattform, die deine Agentur end-to-end betreibt: von der Lead-Suche bis zur Rechnung und Lizenzverwaltung.',
|
||||||
|
highlight: '20 Module · 21 API-Endpunkte · MCP-Server · Client-Portal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Radar,
|
||||||
|
title: 'Leads Finder',
|
||||||
|
description: 'Evidenzbasierte Website-Audits statt geratener KI-Vorschläge. Jeder Score beruht auf gespeicherten Prüfungen — die AI liefert nur die Erklärung.',
|
||||||
|
highlight: 'Approve → Convert → CRM Company + Deal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: FolderKanban,
|
||||||
|
title: 'Projekte & Support',
|
||||||
|
description: 'Aufgaben, Zeitverfolgung, Budgets. Support-Tickets mit AI-Zusammenfassung und Antwort-Vorschlägen (z-ai-web-dev-sdk, server-side).',
|
||||||
|
highlight: 'SLA-Timer · AI Ticket Summary · Billable Time → Invoice',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Receipt,
|
||||||
|
title: 'Finanzen & Buchhaltung',
|
||||||
|
description: 'Rechnungen mit E-Rechnung (ZUGFeRD/XRechnung), Umsatzsteuer-Voranmeldung (UStVA-XML, ELSTER), Bankabgleich (CAMT.053), EÜR, Mahnwesen.',
|
||||||
|
highlight: 'GoBD-konform · DATEV-Export · Hash-Kette Audit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: BookOpen,
|
||||||
|
title: 'Buchhaltung & Fristen',
|
||||||
|
description: 'USt-Voranmeldung mit Kennzahlen (81/86/66/83/37), EÜR, Bankabgleich. Fristen-Kalender aggregiert alle Deadlines aller Module.',
|
||||||
|
highlight: 'Real CAMT.053 Parser · ELSTER XML · SLA-Tracking',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Webhook,
|
||||||
|
title: 'API & MCP Server',
|
||||||
|
description: 'Public REST API mit Scoped Tokens (SHA-256 gehasht). MCP Server (JSON-RPC) für externe Agenten wie Claude Desktop oder ZeroClaw.',
|
||||||
|
highlight: '8 MCP Tools · HMAC-signed Webhooks · OpenAPI 3.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ShieldCheck,
|
||||||
|
title: 'Compliance & DSGVO',
|
||||||
|
description: 'GoBD (Unveränderlichkeit, Hash-Kette), DSGVO (Konsens-Protokoll, Art. 15 Auskunft), BFSG (Barrierefreiheit), UWG § 7 (Cold-Outreach-Gate).',
|
||||||
|
highlight: 'Self-hosted · Daten bleiben im Server',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: CalendarDays,
|
||||||
|
title: 'Client-Portal & i18n',
|
||||||
|
description: 'Kunden sehen nur ihre eigenen Daten (projekt-isoliert). Meilenstein-Freigaben mit IP-Evidenz. UI in DE/EN/TR umschaltbar.',
|
||||||
|
highlight: 'AdES Signatur · Real IP Evidence · 3 Sprachen',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function OnboardingOverlay() {
|
||||||
|
const { onboardingSeen, onboardingStep, setOnboardingSeen, setOnboardingStep } = useAos()
|
||||||
|
|
||||||
|
if (onboardingSeen) return null
|
||||||
|
|
||||||
|
const step = STEPS[onboardingStep]
|
||||||
|
const Icon = step.icon
|
||||||
|
const isLast = onboardingStep === STEPS.length - 1
|
||||||
|
|
||||||
|
const next = () => {
|
||||||
|
if (isLast) {
|
||||||
|
setOnboardingSeen(true)
|
||||||
|
} else {
|
||||||
|
setOnboardingStep(onboardingStep + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prev = () => onboardingStep > 0 && setOnboardingStep(onboardingStep - 1)
|
||||||
|
const skip = () => setOnboardingSeen(true)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
||||||
|
<div className="mx-4 w-full max-w-lg rounded-xl border border-border bg-card shadow-2xl">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between border-b border-border px-6 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="outline" className="text-[10px]">
|
||||||
|
{onboardingStep + 1} / {STEPS.length}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-[10px] uppercase tracking-wide text-muted-foreground">Onboarding</span>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="icon" className="size-7" onClick={skip} aria-label="Überspringen">
|
||||||
|
<X className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress bar */}
|
||||||
|
<div className="h-1 w-full bg-muted">
|
||||||
|
<div
|
||||||
|
className="h-full bg-foreground transition-all duration-300"
|
||||||
|
style={{ width: `${((onboardingStep + 1) / STEPS.length) * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="space-y-4 p-6">
|
||||||
|
<div className="flex size-14 items-center justify-center rounded-xl bg-foreground text-background">
|
||||||
|
<Icon className="size-7" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h2 className="text-xl font-semibold tracking-tight">{step.title}</h2>
|
||||||
|
<p className="text-sm leading-relaxed text-muted-foreground">{step.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 rounded-md border border-dashed border-border bg-muted/30 p-2.5">
|
||||||
|
<Sparkles className="size-3.5 shrink-0 text-muted-foreground" />
|
||||||
|
<code className="text-[11px] text-muted-foreground">{step.highlight}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step dots */}
|
||||||
|
<div className="flex items-center justify-center gap-1.5 pb-2">
|
||||||
|
{STEPS.map((_, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
onClick={() => setOnboardingStep(i)}
|
||||||
|
className={cn(
|
||||||
|
'h-1.5 rounded-full transition-all',
|
||||||
|
i === onboardingStep ? 'w-6 bg-foreground' : i < onboardingStep ? 'w-1.5 bg-foreground/50' : 'w-1.5 bg-muted-foreground/30'
|
||||||
|
)}
|
||||||
|
aria-label={`Schritt ${i + 1}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex items-center justify-between border-t border-border px-6 py-3">
|
||||||
|
<Button variant="ghost" size="sm" onClick={prev} disabled={onboardingStep === 0} className="gap-1">
|
||||||
|
<ChevronLeft className="size-4" />Zurück
|
||||||
|
</Button>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="ghost" size="sm" onClick={skip}>
|
||||||
|
Überspringen
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onClick={next} className="gap-1.5">
|
||||||
|
{isLast ? (
|
||||||
|
<>
|
||||||
|
Los geht's<Check className="size-4" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Weiter<ChevronRight className="size-4" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import { Badge } from '@/components/ui/badge'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { KpiCard, PageHeader, CardSkeletons } from '../primitives'
|
import { KpiCard, PageHeader, CardSkeletons } from '../primitives'
|
||||||
|
import { OnboardingChecklist } from '../onboarding-checklist'
|
||||||
import { formatEuro, formatEuroCompact, formatRelative } from '@/lib/format'
|
import { formatEuro, formatEuroCompact, formatRelative } from '@/lib/format'
|
||||||
import { useAos } from '@/store/aos'
|
import { useAos } from '@/store/aos'
|
||||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis, Cell } from 'recharts'
|
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis, Cell } from 'recharts'
|
||||||
@@ -74,6 +75,9 @@ export function DashboardView() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Onboarding checklist (PRD §9: onboarding checklists, no dead ends) */}
|
||||||
|
<OnboardingChecklist />
|
||||||
|
|
||||||
{/* KPI grid */}
|
{/* KPI grid */}
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<KpiCard
|
<KpiCard
|
||||||
|
|||||||
+14
-1
@@ -44,6 +44,13 @@ interface AosState {
|
|||||||
// i18n (PRD §5.5: de default, en, tr)
|
// i18n (PRD §5.5: de default, en, tr)
|
||||||
locale: Locale
|
locale: Locale
|
||||||
setLocale: (l: Locale) => void
|
setLocale: (l: Locale) => void
|
||||||
|
// onboarding (PRD §9: onboarding checklists, no dead ends)
|
||||||
|
onboardingSeen: boolean
|
||||||
|
onboardingStep: number
|
||||||
|
setOnboardingSeen: (seen: boolean) => void
|
||||||
|
setOnboardingStep: (step: number) => void
|
||||||
|
checklist: Record<string, boolean>
|
||||||
|
toggleChecklist: (key: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAos = create<AosState>()(
|
export const useAos = create<AosState>()(
|
||||||
@@ -65,7 +72,13 @@ export const useAos = create<AosState>()(
|
|||||||
setPortalCompany: (portalCompanyId) => set({ portalCompanyId }),
|
setPortalCompany: (portalCompanyId) => set({ portalCompanyId }),
|
||||||
locale: 'de',
|
locale: 'de',
|
||||||
setLocale: (locale) => set({ locale }),
|
setLocale: (locale) => set({ locale }),
|
||||||
|
onboardingSeen: false,
|
||||||
|
onboardingStep: 0,
|
||||||
|
setOnboardingSeen: (onboardingSeen) => set({ onboardingSeen }),
|
||||||
|
setOnboardingStep: (onboardingStep) => set({ onboardingStep }),
|
||||||
|
checklist: {},
|
||||||
|
toggleChecklist: (key) => set((s) => ({ checklist: { ...s.checklist, [key]: !s.checklist[key] } })),
|
||||||
}),
|
}),
|
||||||
{ name: 'aos-ui', partialize: (s) => ({ view: s.view, portalMode: s.portalMode, portalCompanyId: s.portalCompanyId, locale: s.locale }) }
|
{ name: 'aos-ui', partialize: (s) => ({ view: s.view, portalMode: s.portalMode, portalCompanyId: s.portalCompanyId, locale: s.locale, onboardingSeen: s.onboardingSeen, checklist: s.checklist }) }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Reference in new issue
Block a user