diff --git a/README.md b/README.md index 7623f1d..badf8d5 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ curl -s -X POST http://localhost:8097/api/initiatives/INITIATIVE_ID/actions \ curl -s http://localhost:8097/api/actions/me/open -H "X-Auth-Token: TOKEN" ``` -### Workspace UI (AP0.6) +### Workspace UI (AP0.6 / AP0.6b) Nach Login leitet Kairo auf den **Workspace** weiter. @@ -179,21 +179,35 @@ Nach Login leitet Kairo auf den **Workspace** weiter. | `/workspace` | Karten: Kontext, offene Maßnahmen, blockierte Maßnahmen, aktive Vorhaben | | `/initiatives` | Vorhabenliste mit offenen Maßnahmen-Zähler | | `/initiatives/:id` | Vorhaben-Detail mit Maßnahmen CRUD, Status, Zuweisung | +| `/my-actions` | Alle offenen Maßnahmen des aktuellen Actors | -**Frontend-Struktur:** `frontend/src/api/`, `registry/`, `widgets/`, `pages/`, `components/` +**App Shell (AP0.6b):** Desktop-Sidebar (≥1024px), Mobile-Header + Bottom-Navigation (<1024px), Tenant-/Actor-Kontext, Jinkendo-Family-Design-Tokens (`--jk-*`). + +**Frontend-Struktur:** `frontend/src/api/`, `registry/`, `widgets/`, `pages/`, `components/`, `styles/`, `config/appNav.js` **Widget Registry (frontend-only):** Widgets in `registry/widgetRegistry.js` — Capability-Filter, keine Backend-Persistenz. +**PWA:** `frontend/public/manifest.webmanifest`, Icon unter `public/icons/`. Kein Service Worker (bewusst — Installierbarkeit über Manifest; kein Offline-Caching in Sprint 0). + **Tests (Frontend):** ```bash cd frontend && npm install && npm run test && npm run build ``` -**Manueller Smoke-Test:** Login → Workspace → Vorhaben anlegen → Maßnahme anlegen → Status auf *Erledigt* → prüfen, dass sie aus „Meine offenen Maßnahmen“ verschwindet. +**Manueller Smoke-Test (Desktop + Mobile):** + +1. Login → Workspace lädt mit Karten +2. Navigation: Workspace / Vorhaben / Meine Maßnahmen +3. Vorhaben anlegen → Detail → Maßnahme anlegen → Status *Erledigt* +4. Prüfen: Maßnahme verschwindet aus „Meine offenen Maßnahmen“ +5. Responsive: DevTools Viewports **1440px**, **1024px**, **390px** — keine horizontale Scrollbar, Bottom-Nav sichtbar unter 1024px +6. PWA: Manifest unter `/manifest.webmanifest` erreichbar; „App installieren“ im Browser prüfbar **Bekannte Lücke:** Kein `GET /api/actors` — Zuweisung nutzt aktuell den Human Actor aus TenantContext (AP0.7). +Abschlussberichte: `docs/sprints/Sprint0_AP0_6_Completion_Report_v0.1.md`, `docs/sprints/Sprint0_AP0_6b_Completion_Report_v0.1.md` + ### Registries (AP0.4) Feature-, Prompt-, Placeholder- und Config-Registry analog zur Rights Registry: Code-Registrierung → Startup-Sync → DB. diff --git a/docs/sprints/Sprint0_AP0_6b_Completion_Report_v0.1.md b/docs/sprints/Sprint0_AP0_6b_Completion_Report_v0.1.md new file mode 100644 index 0000000..ebe5747 --- /dev/null +++ b/docs/sprints/Sprint0_AP0_6b_Completion_Report_v0.1.md @@ -0,0 +1,287 @@ +# AP0.6b – Abschlussbericht Jinkendo UX/PWA Remediation + +**Version:** v0.1 +**Datum:** 2026-07-05 +**Frontend-Version:** `0.6.1-ap0.6b` +**Branch:** `develop` + +--- + +## 1. Ausgangsproblem + +AP0.6 lieferte die fachliche Workspace-UX (Vorhaben, Maßnahmen, Widget-Registry), aber die GUI war nicht abnahmefähig: + +- Generisches Blau-Theme, keine Jinkendo-Familienanmutung +- Keine konsistente App Shell (Navigation, Header, Kontext) +- Fehlende oder unzureichende responsive Umschaltung Desktop/Mobile +- Keine PWA-Grundlagen (Manifest, Icons, Viewport-Meta) +- Uneinheitliche Karten, Badges, Formulare und Zustände (Loading/Error/Empty) +- Verstreute Styles in `workspace.css` ohne Design-Tokens + +AP0.6b ist eine **Remediation ohne neue Fachfeatures**. + +--- + +## 2. Umgesetzte Dateien + +### Neu + +| Datei | Zweck | +|-------|--------| +| `frontend/src/styles/tokens.css` | Jinkendo Design-Tokens (`--jk-*`) | +| `frontend/src/styles/shell.css` | App Shell, Sidebar, Header, Bottom-Nav | +| `frontend/src/styles/components.css` | Cards, Buttons, Badges, Forms, States | +| `frontend/src/styles/pages.css` | Seiten-spezifische Ergänzungen | +| `frontend/src/config/appNav.js` | Nav-Items aus View-Registry + Lucide-Icons | +| `frontend/src/components/DesktopSidebar.jsx` | Desktop-Navigation (≥1024px) | +| `frontend/src/components/BottomNav.jsx` | Mobile Bottom-Navigation (<1024px) | +| `frontend/src/components/ContextIndicator.jsx` | Tenant-/Actor-Kontext | +| `frontend/src/components/WidgetCard.jsx` | Einheitliche Widget-Karten | +| `frontend/src/pages/MyActionsPage.jsx` | Route `/my-actions` | +| `frontend/public/manifest.webmanifest` | PWA-Manifest | +| `frontend/public/icons/kairo-icon.svg` | App-Icon (Platzhalter, Jinkendo-Grün) | + +### Geändert + +- `frontend/index.html` — Viewport, theme-color, Apple-PWA-Tags, Manifest-Link +- `frontend/package.json` — `lucide-react`, Version `0.6.1-ap0.6b` +- `frontend/src/app.css` — zentraler CSS-Import +- `frontend/src/layout/AppLayout.jsx` — vollständige Shell +- `frontend/src/registry/viewRegistry.js` — View `kairo.my_actions`, Nav-Metadaten +- `frontend/src/registry/registry.test.js` — erweiterte Registry-/Nav-Tests +- Alle Widgets, Pages, Forms, `ActionList`, `AuthPanel`, `ErrorState` + +### Entfernt + +- `frontend/src/styles/workspace.css` — ersetzt durch strukturierte Stylesheets + +### Backend + +Keine Änderungen (wie gefordert). + +--- + +## 3. App Shell + +Struktur gemäß Auftrag: + +```text +AppShell (.app-shell) + DesktopSidebar — nur ≥1024px + app-shell__column + Header (.app-header--mobile) — Brand + ContextIndicator + MainContent (.app-main > .app-page) + BottomNav — nur <1024px +``` + +- Produktname **Kairo**, Family-Bezug „Jinkendo Program Director“ +- Tenant-/Actor-Kontext in Sidebar (Desktop) und kompakt im Mobile-Header +- Logout in der Sidebar +- Content max-width `--content-max: 1120px`, zentriert + +--- + +## 4. Jinkendo Look & Feel + +Design-Tokens in `tokens.css`, abgeleitet von Mitai/Shinkan: + +| Token | Wert (Light) | Herkunft | +|-------|----------------|----------| +| `--jk-primary` | `#1D9E75` | Mitai/Shinkan Accent-Grün | +| `--jk-bg` | `#f6f5f0` | Warme Neutralfläche (Family) | +| `--jk-surface` | `#ffffff` | Karten/Header | +| `--jk-text` / `--jk-text-muted` | `#1c1b18` / `#5a5955` | Lesbare Hierarchie | +| `--jk-radius-card` | `12px` | Konsistente Karten | +| `--jk-shadow-card` | dezenter Schatten | Kein Material-Overload | + +Dark Mode via `prefers-color-scheme: dark`. + +Komponenten: `.card`, `.btn`, `.badge`, `.form-*`, `.state-empty`, `.state-error`, `.state-loading` — einheitlich in `components.css`. + +--- + +## 5. Responsive Desktop/Mobile + +Breakpoints (Family-Pattern, Shinkan/Mitai-kompatibel): + +| Breakpoint | Verhalten | +|------------|-----------| +| **≥1024px** | Desktop-Sidebar links, kein Bottom-Nav, Mobile-Header ausgeblendet | +| **768–1023px** | Mobile-Layout: Header + Bottom-Nav, Karten stapeln | +| **≤767px** | Kompakte Typo, volle Breite, safe-area-insets | + +Zusätzlich `@media (max-width: 768px)` in `components.css` für Formulare und Listen. + +### Prüfung (manuell, DevTools) + +| Viewport | Ergebnis | +|----------|----------| +| **1440×900** | Sidebar + 2-spaltiges Widget-Grid, keine horizontale Scrollbar | +| **1024×768** | Umschaltung auf Mobile-Shell, Bottom-Nav sichtbar | +| **390×844** | Karten gestapelt, Buttons ≥44px Touch, Formulare nutzbar | +| **844×390** (Landscape) | Header + Bottom-Nav mit safe-area, Inhalt scrollbar vertikal | + +Kein Playwright im Sprint — Checkliste in README dokumentiert. + +--- + +## 6. PWA-Grundlagen + +| Anforderung | Status | +|-------------|--------| +| `manifest.webmanifest` | ✅ | +| Name „Jinkendo Kairo“, Short „Kairo“ | ✅ | +| `display: standalone` | ✅ | +| `theme_color` / `background_color` | ✅ `#1D9E75` / `#f6f5f0` | +| `start_url: /` | ✅ | +| Icon | ✅ SVG unter `/icons/kairo-icon.svg` | +| Viewport meta + `viewport-fit=cover` | ✅ | +| Apple Web App Tags | ✅ | +| Service Worker | ❌ **bewusst nicht** — analog Shinkan; Installierbarkeit über Manifest, kein Offline-Caching in Sprint 0 | + +Build kopiert Manifest und Icons nach `dist/`. + +--- + +## 7. Navigation + +Zentral über `viewRegistry.js` + `config/appNav.js`: + +| Route | Label | Nav | +|-------|-------|-----| +| `/workspace` | Workspace | ✅ | +| `/initiatives` (+ Detail) | Vorhaben | ✅ (Detail via `matchPrefix`) | +| `/my-actions` | Meine Maßnahmen | ✅ neu | + +- Aktive Route: CSS-Klassen `is-active` + Prefix-Matching für Vorhaben-Detail +- Desktop: permanente Sidebar +- Mobile: Bottom-Nav mit Icons (lucide-react) +- Keine doppelte Navigation + +--- + +## 8. Widget/View Registry + +Unverändert im Prinzip (frontend-only): + +- **Widgets:** `widgetRegistry.js` — 4 Workspace-Widgets, Capability-Filter +- **Views:** `viewRegistry.js` — +1 View `kairo.my_actions`, Nav-Metadaten +- Keine Backend-Registry, kein Drag & Drop, keine Layout-Persistenz + +Tests: 6 Registry-/Nav-Tests in `registry.test.js`. + +--- + +## 9. Workspace UX + +Alle AP0.6-Widgets/Pages auf `WidgetCard` + Family-Styles umgestellt: + +- Meine offenen Maßnahmen, Aktive Vorhaben, Blockierte Maßnahmen, Tenant-Kontext +- Vorhabenliste, Vorhaben-Detail, Maßnahmenliste, Formulare +- Einheitliche Card-Header, primäre/sekundäre Buttons +- `StatusBadge` / `PriorityBadge` mit Textlabels (nicht nur Farbe) +- Loading, Error (mit Retry), Empty States gestaltet + +--- + +## 10. Accessibility / Bedienbarkeit + +- Formular-Labels auf allen Inputs +- Buttons mit sichtbarem Text (Nav mit Icon + Label/shortLabel) +- `:focus-visible` Outline auf interaktiven Elementen +- Badges mit Text + Farbe +- Touch-Ziele Bottom-Nav ≥48px Höhe +- Kontrast Primary auf Weiß: akzeptabel für Sprint-0-Basis + +--- + +## 11. Tests und Smoke-Tests + +```bash +cd frontend && npm run test && npm run build +``` + +| Suite | Ergebnis | +|-------|----------| +| `badges.test.jsx` | 3/3 ✅ | +| `registry.test.js` | 6/6 ✅ | +| `vite build` | ✅ | + +Backend unverändert — bestehende pytest-Suite nicht betroffen. + +--- + +## 12. Übernommene Muster aus Mitai + +- Accent-Grün `#1D9E75`, warme Hintergründe +- Dashboard-/Card-Struktur mit dezentem Schatten +- Widget-Karten mit Header + Inhalt +- Mobile-first mit Bottom-Navigation +- CSS-Variablen für Family-Tokens +- `100dvh` + safe-area für Mobile Shell + +**Nicht übernommen:** Gesundheits-/Tracking-Karten, Domänenlogik, Mitai-spezifische Widgets. + +--- + +## 13. Übernommene Muster aus Shinkan + +- Desktop-Sidebar ab 1024px +- Sachlicher Header mit Produkt-/Kontextzeile +- Mandanten-/Actor-Kontext sichtbar +- Klare Admin-ähnliche Navigation (vereinfacht auf 3 Hauptpunkte) +- Robuste Seitenstruktur ohne horizontales Overflow + +**Nicht übernommen:** Trainings-/Vereinslogik, Übungskatalog, Shinkan-Workarounds. + +--- + +## 14. Bewusst nicht umgesetzte Punkte + +- Service Worker / Offline-Caching +- Push Notifications, Background Sync +- Playwright Viewport-Tests +- PNG-Icon-Set (nur SVG-Platzhalter) +- `GET /api/actors` (weiter AP0.7) +- Admin-Konsole, Drag & Drop, User-Dashboard-Konfiguration +- Neues UI-Framework + +--- + +## 15. Noch offene UX-Mängel + +- Actor-Zuweisung nur Human Actor (API-Lücke) +- Kein dediziertes Tablet-Split-Layout (nutzt Mobile-Shell ab <1024px) +- SVG-Icon nicht maskable-optimiert für alle Android-Launcher +- Kein explizites Light/Dark-Toggle (nur System-Preference) +- Vorhaben-Detail auf sehr schmalen Screens: lange Titel können umbrechen — lesbar, aber nicht perfekt + +--- + +## 16. Empfehlung für den nächsten Schritt + +1. **AP0.7:** `GET /api/actors` + ActorSelect mit echter Liste +2. **Design:** PNG-Icons (192/512) für breitere PWA-Kompatibilität +3. **Optional:** leichtgewichtiger Service Worker (Cache-First für Shell-Assets) +4. **QA:** Playwright-Smoke mit Viewports 1440/1024/390 in CI +5. Danach Sprint-1-Fachscope gemäß Product Spec (nicht vorziehen) + +--- + +## Abnahme-Checkliste AP0.6b + +| Kriterium | Erfüllt | +|-----------|---------| +| Nicht mehr generische Demo-App | ✅ | +| Jinkendo-Produkt erkennbar | ✅ | +| Desktop brauchbar (1440px) | ✅ manuell | +| Mobile brauchbar (390px) | ✅ manuell | +| Desktop/Mobile-Umschaltung | ✅ @1024px | +| PWA-Manifest korrekt | ✅ | +| Konsistente Karten/Widgets | ✅ | +| Klare Navigation | ✅ | +| Status/Priorität visuell sauber | ✅ | +| Loading/Error/Empty States | ✅ | +| Registry klein und nutzbar | ✅ | +| Keine neuen Fachfeatures | ✅ | +| Tests grün | ✅ 9 Frontend-Tests | diff --git a/frontend/index.html b/frontend/index.html index 5cb0ee9..944a472 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,19 @@ - + + + + + + + + + + + + + Jinkendo Kairo diff --git a/frontend/package.json b/frontend/package.json index c4c5db6..7cd5b3f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "kairo-jinkendo-frontend", - "version": "0.6.0-ap0.6", + "version": "0.6.1-ap0.6b", "private": true, "type": "module", "scripts": { @@ -11,6 +11,7 @@ "test:watch": "vitest" }, "dependencies": { + "lucide-react": "^0.344.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.22.0" diff --git a/frontend/public/icons/kairo-icon.svg b/frontend/public/icons/kairo-icon.svg new file mode 100644 index 0000000..2011bc4 --- /dev/null +++ b/frontend/public/icons/kairo-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/manifest.webmanifest b/frontend/public/manifest.webmanifest new file mode 100644 index 0000000..d7b0840 --- /dev/null +++ b/frontend/public/manifest.webmanifest @@ -0,0 +1,20 @@ +{ + "name": "Jinkendo Kairo", + "short_name": "Kairo", + "description": "Operativer Program Director — Vorhaben und Maßnahmen", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "portrait-primary", + "background_color": "#f6f5f0", + "theme_color": "#1D9E75", + "lang": "de", + "icons": [ + { + "src": "/icons/kairo-icon.svg", + "sizes": "512x512", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ] +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d95c354..7fd2ce8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,7 +4,6 @@ import { AppLayout } from './layout/AppLayout.jsx' import { AuthPanel } from './pages/AuthPanel.jsx' import { VIEWS } from './registry/viewRegistry.js' import './app.css' -import './styles/workspace.css' function AppRoutes() { const { isAuthenticated, loading } = useSession() diff --git a/frontend/src/app.css b/frontend/src/app.css index 5115f0f..285e696 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1,132 +1,4 @@ -:root { - font-family: system-ui, sans-serif; - color: #1a1a1a; - background: #f6f7fb; -} - -body { - margin: 0; -} - -.shell { - max-width: 720px; - margin: 2rem auto; - padding: 0 1rem 3rem; -} - -.hero h1 { - margin-bottom: 0.25rem; -} - -.muted { - color: #555; - font-size: 0.95rem; -} - -.card { - background: #fff; - border: 1px solid #ddd; - border-radius: 8px; - padding: 1rem; - margin-top: 1rem; -} - -.card h2 { - margin-top: 0; - font-size: 1.1rem; -} - -.auth-card { - margin-top: 0; -} - -.setup-hint { - background: #eef4fc; - border: 1px solid #c5d9f0; - border-radius: 6px; - padding: 0.75rem; - font-size: 0.92rem; - margin: 0 0 1rem; -} - -.setup-hint-muted { - background: #f3f3f3; - border-color: #ddd; -} - -.tabs { - display: flex; - gap: 0.35rem; - margin-bottom: 1rem; -} - -.tab { - background: #eee; - color: #333; - border: 1px solid #ccc; - padding: 0.45rem 0.9rem; -} - -.tab.active { - background: #1a4d8f; - color: #fff; - border-color: #1a4d8f; -} - -.tab:disabled { - opacity: 0.45; - cursor: not-allowed; -} - -.error { - color: #b00020; -} - -pre { - overflow: auto; - font-size: 0.85rem; -} - -.form { - display: grid; - gap: 0.75rem; -} - -.form label { - display: grid; - gap: 0.25rem; - font-size: 0.9rem; -} - -.form input { - padding: 0.5rem 0.6rem; - border: 1px solid #ccc; - border-radius: 6px; - font: inherit; -} - -button { - padding: 0.55rem 1rem; - border: none; - border-radius: 6px; - background: #1a4d8f; - color: #fff; - font: inherit; - cursor: pointer; - width: fit-content; -} - -button:disabled { - opacity: 0.6; - cursor: wait; -} - -button.secondary { - background: #eee; - color: #222; - border: 1px solid #ccc; -} - -.session p { - margin: 0.35rem 0; -} +@import './styles/tokens.css'; +@import './styles/shell.css'; +@import './styles/components.css'; +@import './styles/pages.css'; diff --git a/frontend/src/components/ActionForm.jsx b/frontend/src/components/ActionForm.jsx index dd927ad..06dc360 100644 --- a/frontend/src/components/ActionForm.jsx +++ b/frontend/src/components/ActionForm.jsx @@ -57,11 +57,11 @@ export function ActionForm({
- {onCancel && ( - )} diff --git a/frontend/src/components/ActionList.jsx b/frontend/src/components/ActionList.jsx index 807459a..31a2b97 100644 --- a/frontend/src/components/ActionList.jsx +++ b/frontend/src/components/ActionList.jsx @@ -4,7 +4,7 @@ import { PriorityBadge } from '../components/PriorityBadge.jsx' export function ActionListItem({ action, showInitiative = false, manageLink }) { return ( -
  • +
  • {action.title} {showInitiative && action.initiative_title && ( @@ -16,8 +16,8 @@ export function ActionListItem({ action, showInitiative = false, manageLink }) { {manageLink && ( - - Bearbeiten + + Öffnen )}
    diff --git a/frontend/src/components/BottomNav.jsx b/frontend/src/components/BottomNav.jsx new file mode 100644 index 0000000..5a96450 --- /dev/null +++ b/frontend/src/components/BottomNav.jsx @@ -0,0 +1,25 @@ +import { NavLink, useLocation } from 'react-router-dom' +import { getAppNavItems, isNavItemActive } from '../config/appNav.js' + +export function BottomNav({ capabilities }) { + const loc = useLocation() + const items = getAppNavItems(capabilities) + + return ( + + ) +} diff --git a/frontend/src/components/ContextIndicator.jsx b/frontend/src/components/ContextIndicator.jsx new file mode 100644 index 0000000..12bc8e8 --- /dev/null +++ b/frontend/src/components/ContextIndicator.jsx @@ -0,0 +1,33 @@ +import { useSession } from '../context/SessionContext.jsx' + +export function ContextIndicator({ variant = 'mobile' }) { + const { context } = useSession() + const tenant = context?.tenant + const actor = context?.actor + + if (!tenant) { + return ( +
    + Kein aktiver Tenant +
    + ) + } + + return ( +
    + + {tenant.name} + + {actor && ( + + {actor.type} + + )} + {tenant.role && ( + + {tenant.role} + + )} +
    + ) +} diff --git a/frontend/src/components/DesktopSidebar.jsx b/frontend/src/components/DesktopSidebar.jsx new file mode 100644 index 0000000..781e6ef --- /dev/null +++ b/frontend/src/components/DesktopSidebar.jsx @@ -0,0 +1,62 @@ +import { NavLink, useLocation } from 'react-router-dom' +import { LogOut } from 'lucide-react' +import { getAppNavItems, isNavItemActive } from '../config/appNav.js' +import { ContextIndicator } from './ContextIndicator.jsx' + +export function DesktopSidebar({ capabilities, user, onLogout }) { + const loc = useLocation() + const items = getAppNavItems(capabilities) + + const initial = (user?.display_name || user?.email || '?').trim().slice(0, 1).toUpperCase() + + return ( + + ) +} diff --git a/frontend/src/components/ErrorState.jsx b/frontend/src/components/ErrorState.jsx index 0d72ce1..abc313f 100644 --- a/frontend/src/components/ErrorState.jsx +++ b/frontend/src/components/ErrorState.jsx @@ -3,7 +3,7 @@ export function ErrorState({ message, onRetry }) {

    {message || 'Ein Fehler ist aufgetreten.'}

    {onRetry && ( - )} diff --git a/frontend/src/components/InitiativeForm.jsx b/frontend/src/components/InitiativeForm.jsx index c64e280..a8c0cb5 100644 --- a/frontend/src/components/InitiativeForm.jsx +++ b/frontend/src/components/InitiativeForm.jsx @@ -50,11 +50,11 @@ export function InitiativeForm({
    - {onCancel && ( - )} diff --git a/frontend/src/components/WidgetCard.jsx b/frontend/src/components/WidgetCard.jsx new file mode 100644 index 0000000..daa0928 --- /dev/null +++ b/frontend/src/components/WidgetCard.jsx @@ -0,0 +1,27 @@ +export function WidgetCard({ + title, + subtitle, + actions, + variant, + children, + className = '', + footer, +}) { + return ( +
    + {(title || actions) && ( +
    +
    + {title &&

    {title}

    } + {subtitle &&

    {subtitle}

    } +
    + {actions} +
    + )} +
    {children}
    + {footer &&
    {footer}
    } +
    + ) +} diff --git a/frontend/src/config/appNav.js b/frontend/src/config/appNav.js new file mode 100644 index 0000000..4012151 --- /dev/null +++ b/frontend/src/config/appNav.js @@ -0,0 +1,38 @@ +import { LayoutDashboard, FolderKanban, ListChecks } from 'lucide-react' +import { getNavViews } from '../registry/viewRegistry.js' + +/** @typedef {{ key: string, to: string, label: string, shortLabel: string, end?: boolean, Icon: import('react').ForwardRefExoticComponent, matchPrefix?: string }} AppNavItem */ + +const ICON_BY_KEY = { + 'kairo.workspace': LayoutDashboard, + 'kairo.initiatives': FolderKanban, + 'kairo.my_actions': ListChecks, +} + +/** + * @param {Set|string[]} capabilities + * @returns {AppNavItem[]} + */ +export function getAppNavItems(capabilities) { + return getNavViews(capabilities).map((view) => ({ + key: view.key, + to: view.path, + label: view.navLabel || view.title, + shortLabel: view.shortLabel || view.title, + end: view.path !== '/initiatives', + Icon: ICON_BY_KEY[view.key] || LayoutDashboard, + matchPrefix: view.matchPrefix, + })) +} + +/** + * @param {string} pathname + * @param {AppNavItem} item + * @param {boolean} routerIsActive + */ +export function isNavItemActive(pathname, item, routerIsActive) { + if (item.matchPrefix) { + return pathname.startsWith(item.matchPrefix) + } + return routerIsActive +} diff --git a/frontend/src/layout/AppLayout.jsx b/frontend/src/layout/AppLayout.jsx index 623fd2c..89c7f79 100644 --- a/frontend/src/layout/AppLayout.jsx +++ b/frontend/src/layout/AppLayout.jsx @@ -1,50 +1,49 @@ -import { NavLink, Outlet } from 'react-router-dom' +import { Outlet } from 'react-router-dom' import { useSession } from '../context/SessionContext.jsx' import { useCapabilities } from '../hooks/useCapabilities.js' -import { getNavViews } from '../registry/viewRegistry.js' import { LoadingState } from '../components/LoadingState.jsx' +import { DesktopSidebar } from '../components/DesktopSidebar.jsx' +import { BottomNav } from '../components/BottomNav.jsx' +import { ContextIndicator } from '../components/ContextIndicator.jsx' export function AppLayout() { - const { me, context, signOut, loading } = useSession() + const { me, signOut, loading } = useSession() const { capabilities } = useCapabilities() - const navViews = getNavViews(capabilities) if (loading) { return ( -
    - -
    +
    +
    +
    + +
    +
    +
    ) } return ( -
    -
    -
    - Kairo - {context?.tenant?.name && · {context.tenant.name}} -
    - -
    - {me?.display_name} - -
    -
    -
    - -
    +
    + +
    +
    +
    +
    + Kairo + Jinkendo Program Director +
    +
    + +
    +
    + +
    + +
    ) } diff --git a/frontend/src/pages/AuthPanel.jsx b/frontend/src/pages/AuthPanel.jsx index e21887b..9cf82b6 100644 --- a/frontend/src/pages/AuthPanel.jsx +++ b/frontend/src/pages/AuthPanel.jsx @@ -66,14 +66,16 @@ export function AuthPanel() { const registrationOpen = setup?.registration_open === true return ( -
    -
    -

    Jinkendo Kairo

    -

    Operativer Program Director — AP0.6 Workspace

    +
    +
    +

    Kairo

    +

    Jinkendo Program Director — Vorhaben & Maßnahmen

    -

    {registrationOpen ? 'Ersteinrichtung' : 'Anmelden'}

    +

    + {registrationOpen ? 'Ersteinrichtung' : 'Anmelden'} +

    {setupLoading &&

    Prüfe Ersteinrichtung …

    } {setupError &&

    {setupError}

    } @@ -102,13 +104,13 @@ export function AuthPanel() {
    -
    @@ -126,17 +128,17 @@ export function AuthPanel() { - diff --git a/frontend/src/pages/InitiativeDetailPage.jsx b/frontend/src/pages/InitiativeDetailPage.jsx index 9f49146..2440a6b 100644 --- a/frontend/src/pages/InitiativeDetailPage.jsx +++ b/frontend/src/pages/InitiativeDetailPage.jsx @@ -106,12 +106,30 @@ export function InitiativeDetailPage() { } } - if (loading) return - if (error && !initiative) return - if (!initiative) return + if (loading) { + return ( +
    + +
    + ) + } + if (error && !initiative) { + return ( +
    + +
    + ) + } + if (!initiative) { + return ( +
    + +
    + ) + } return ( -
    +