79ecf134-0188-4613-b691-50e43114247e

This commit is contained in:
Z User committed 2026-10-27 00:54:10 +00:00
1 parent 9b5477020c
commit 8fa25577c0
10 files changed
+117 -1

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

+44
View File
@@ -0,0 +1,44 @@
{
"name": "AOS — Agency Operating System",
"short_name": "AOS",
"description": "Selbstgehostete Plattform für Software-Agenturen: CRM, Leads, Projekte, Lizenzen, Finanzen — mit EU/DE-Compliance.",
"start_url": "/",
"display": "standalone",
"background_color": "#0a0a0a",
"theme_color": "#0a0a0a",
"orientation": "any",
"categories": ["business", "productivity", "finance"],
"lang": "de",
"dir": "ltr",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"shortcuts": [
{
"name": "Leads prüfen",
"url": "/?view=leads",
"description": "Leads Finder Review-Queue"
},
{
"name": "Zeit erfassen",
"url": "/?view=projects",
"description": "Projekt-Zeiterfassung"
},
{
"name": "Freigaben",
"url": "/?view=dashboard",
"description": "Offene Meilenstein-Freigaben"
}
]
}
+7
View File
@@ -29,8 +29,15 @@ export const metadata: Metadata = {
"GoBD",
],
authors: [{ name: "Nordlicht Digital GmbH" }],
manifest: "/manifest.json",
appleWebApp: {
capable: true,
title: "AOS",
statusBarStyle: "black",
},
icons: {
icon: "https://z-cdn.chatglm.cn/z-ai/static/logo.svg",
apple: "https://z-cdn.chatglm.cn/z-ai/static/logo.svg",
},
openGraph: {
title: "AOS — Agency Operating System",
+3 -1
View File
@@ -28,6 +28,7 @@ import { GlobalSettingsView } from './views/global-settings'
import { SetupWizardView } from './views/setup-wizard'
import { PortalShell } from './portal-shell'
import { OnboardingOverlay } from './onboarding-overlay'
import { MobileBottomNav } from './mobile-bottom-nav'
export function AppShell() {
const { view, sidebarOpen, setSidebarOpen, portalMode } = useAos()
@@ -55,7 +56,7 @@ export function AppShell() {
{/* Main */}
<div className="flex min-w-0 flex-1 flex-col">
<TopBar />
<main className="flex-1">
<main className="flex-1 pb-16 md:pb-0">
{view === 'dashboard' && <DashboardView />}
{view === 'calendar' && <CalendarView />}
{view === 'reporting' && <ReportingView />}
@@ -82,6 +83,7 @@ export function AppShell() {
<CommandMenu />
<CompanyDrawer />
<OnboardingOverlay />
<MobileBottomNav />
</div>
)
}
+63
View File
@@ -0,0 +1,63 @@
'use client'
import { useAos, type ViewId } from '@/store/aos'
import { LayoutDashboard, Radar, FolderKanban, Bell, Menu } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useFetch } from '@/hooks/use-fetch'
// Mobile bottom navigation bar (PRD §9: "Mobile: usable PWA for time tracking,
// approvals and quick CRM lookups"). Shows on mobile only (md:hidden).
// Provides 5 quick-access destinations optimized for touch.
export function MobileBottomNav() {
const { view, setView, setSidebarOpen, setCommandOpen } = useAos()
const { data } = useFetch<{ stats: { total: number; urgent: number } }>('/api/aos/notifications')
const urgent = data?.stats.urgent ?? 0
const total = data?.stats.total ?? 0
const items: { id: ViewId | 'search' | 'menu'; label: string; icon: typeof LayoutDashboard; badge?: number }[] = [
{ id: 'dashboard', label: 'Home', icon: LayoutDashboard },
{ id: 'leads', label: 'Leads', icon: Radar },
{ id: 'search', label: 'Suche', icon: Menu },
{ id: 'projects', label: 'Zeit', icon: FolderKanban },
{ id: 'notifications', label: 'Alerts', icon: Bell, badge: urgent > 0 ? urgent : total > 0 ? total : undefined },
]
const handleClick = (id: typeof items[number]['id']) => {
if (id === 'search') setCommandOpen(true)
else if (id === 'notifications') setView('calendar')
else if (id === 'menu') setSidebarOpen(true)
else setView(id as ViewId)
}
return (
<nav className="fixed bottom-0 left-0 right-0 z-40 flex h-16 items-stretch border-t border-border bg-background/95 backdrop-blur-md md:hidden">
{items.map((item) => {
const Icon = item.icon
const active = view === (item.id === 'notifications' ? 'calendar' : item.id === 'search' || item.id === 'menu' ? '' : item.id)
return (
<button
key={item.id}
onClick={() => handleClick(item.id)}
className={cn(
'relative flex flex-1 flex-col items-center justify-center gap-0.5 text-[10px] font-medium transition',
active ? 'text-foreground' : 'text-muted-foreground'
)}
aria-label={item.label}
>
<div className="relative">
<Icon className={cn('size-5', active && 'text-foreground')} />
{item.badge !== undefined && item.badge > 0 && (
<span className="absolute -right-2 -top-1.5 flex size-4 items-center justify-center rounded-full bg-foreground text-[8px] font-bold text-background">
{item.badge > 9 ? '9+' : item.badge}
</span>
)}
</div>
<span>{item.label}</span>
{active && <span className="absolute bottom-0 left-1/2 h-0.5 w-8 -translate-x-1/2 rounded-full bg-foreground" />}
</button>
)
})}
</nav>
)
}