79ecf134-0188-4613-b691-50e43114247e
This commit is contained in:
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 |
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -29,8 +29,15 @@ export const metadata: Metadata = {
|
|||||||
"GoBD",
|
"GoBD",
|
||||||
],
|
],
|
||||||
authors: [{ name: "Nordlicht Digital GmbH" }],
|
authors: [{ name: "Nordlicht Digital GmbH" }],
|
||||||
|
manifest: "/manifest.json",
|
||||||
|
appleWebApp: {
|
||||||
|
capable: true,
|
||||||
|
title: "AOS",
|
||||||
|
statusBarStyle: "black",
|
||||||
|
},
|
||||||
icons: {
|
icons: {
|
||||||
icon: "https://z-cdn.chatglm.cn/z-ai/static/logo.svg",
|
icon: "https://z-cdn.chatglm.cn/z-ai/static/logo.svg",
|
||||||
|
apple: "https://z-cdn.chatglm.cn/z-ai/static/logo.svg",
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: "AOS — Agency Operating System",
|
title: "AOS — Agency Operating System",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ 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'
|
import { OnboardingOverlay } from './onboarding-overlay'
|
||||||
|
import { MobileBottomNav } from './mobile-bottom-nav'
|
||||||
|
|
||||||
export function AppShell() {
|
export function AppShell() {
|
||||||
const { view, sidebarOpen, setSidebarOpen, portalMode } = useAos()
|
const { view, sidebarOpen, setSidebarOpen, portalMode } = useAos()
|
||||||
@@ -55,7 +56,7 @@ export function AppShell() {
|
|||||||
{/* Main */}
|
{/* Main */}
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
<TopBar />
|
<TopBar />
|
||||||
<main className="flex-1">
|
<main className="flex-1 pb-16 md:pb-0">
|
||||||
{view === 'dashboard' && <DashboardView />}
|
{view === 'dashboard' && <DashboardView />}
|
||||||
{view === 'calendar' && <CalendarView />}
|
{view === 'calendar' && <CalendarView />}
|
||||||
{view === 'reporting' && <ReportingView />}
|
{view === 'reporting' && <ReportingView />}
|
||||||
@@ -82,6 +83,7 @@ export function AppShell() {
|
|||||||
<CommandMenu />
|
<CommandMenu />
|
||||||
<CompanyDrawer />
|
<CompanyDrawer />
|
||||||
<OnboardingOverlay />
|
<OnboardingOverlay />
|
||||||
|
<MobileBottomNav />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user