From c8fc024ca0eeca725e861f03c7e3b94e2827259d Mon Sep 17 00:00:00 2001 From: DeeCee03 Date: Tue, 2 Jun 2026 17:51:08 +0200 Subject: [PATCH] =?UTF-8?q?Puls=20=E2=86=92=20Dashboard=20umbenennen=20+?= =?UTF-8?q?=20Projekt-Beschreibung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI/Routing - Tab + Route: /puls → /dashboard, Heading „Unternehmens-Puls" → „Dashboard" - Shortcut: g p → g d (Cheatsheet entsprechend) - Breadcrumbs, 404-Hinweistext, alle internen Links angepasst Code-Layout - apps/web/src/routes/puls.tsx → dashboard.tsx (Komponente DashboardRoute) - apps/api/src/routers/puls.ts → dashboard.ts (dashboardRouter) - tRPC-Aufrufe: trpc.puls.* / utils.puls.* → trpc.dashboard.* / utils.dashboard.* - Auth-Redirects (/api/auth/login, /callback, dev-login) zeigen jetzt auf /dashboard Projekt-Beschreibung - Migration 0007_project_description: project.description text (nullable) - Shared Zod: ProjectSchemas.createProjectInput.description (max 2000, optional) - Backend: project.create mappt input.description - Frontend Dialog „Neues Projekt": Textarea (3 Zeilen, optional) - Projekt-Detail: zeigt Beschreibung als whitespace-pre-wrap unter Subtitle README - Routen-Tabelle, Shortcut-Liste, Verzeichnis-Baum, DB-Tabellen, Migration-Range, Frontend-Routenliste aktualisiert Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 22 +- apps/api/src/auth/routes.ts | 8 +- apps/api/src/routers/_app.ts | 4 +- .../api/src/routers/{puls.ts => dashboard.ts} | 2 +- apps/api/src/routers/day-entry.ts | 2 +- apps/api/src/routers/project.ts | 1 + .../src/components/blocker/BlockerDialog.tsx | 2 +- apps/web/src/components/layout/AppShell.tsx | 4 +- apps/web/src/components/layout/TopNav.tsx | 4 +- apps/web/src/lib/router.tsx | 6 +- apps/web/src/routes/bereich.tsx | 14 +- .../src/routes/{puls.tsx => dashboard.tsx} | 6 +- apps/web/src/routes/heute.tsx | 2 +- apps/web/src/routes/not-found.tsx | 6 +- apps/web/src/routes/projekt.tsx | 11 +- apps/web/src/routes/reflexion.tsx | 2 +- .../migrations/0007_project_description.sql | 1 + .../db/migrations/meta/0007_snapshot.json | 1958 +++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/src/schema.ts | 1 + packages/shared/src/schemas/project.ts | 1 + 21 files changed, 2025 insertions(+), 39 deletions(-) rename apps/api/src/routers/{puls.ts => dashboard.ts} (98%) rename apps/web/src/routes/{puls.tsx => dashboard.tsx} (98%) create mode 100644 packages/db/migrations/0007_project_description.sql create mode 100644 packages/db/migrations/meta/0007_snapshot.json diff --git a/README.md b/README.md index 462d19c..d04dcc2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Internes Web-Tool von **ToolCTRL** für den täglichen Überblick über alle Pro | Bereich | Feature | |---|---| -| **Puls** (`/puls`) | Unternehmens-Übersicht: Bereichs-Ampelstatus, „Heute fehlt"-Liste, bereichsübergreifende Blocker, Pflicht-Minimum-Übersicht, Jourfixe-Hinweis, „Heute im Team" (ToDos), „Zeit im Team heute" (Live-Tracking), Vortags-Feed | +| **Dashboard** (`/dashboard`) | Unternehmens-Übersicht: Bereichs-Ampelstatus, „Heute fehlt"-Liste, bereichsübergreifende Blocker, Pflicht-Minimum-Übersicht, Jourfixe-Hinweis, „Heute im Team" (ToDos), „Zeit im Team heute" (Live-Tracking), Vortags-Feed | | **Bereich** (`/bereich`, `/bereich/`) | Tab-Switcher über Entwicklung / Vertrieb / Marketing — pro Bereich Projekt-Liste (Kundenprojekte / Interne Projekte) mit Pflicht-Minimum-Indicator | | **Projekt** (`/projekt/:id`) | Detail: Verlinkungen (Outline-Steckbrief, Plane-Board, Risikoliste), Pflicht-Minimum-Status, Meilenstein-Checkliste, DoD, Projekt-Abschluss (P1) | | **Heute** (`/heute`) | Tagesrhythmus: Lock-in mit ToDo-Liste, Auto-Carryover offener ToDos vom Vortag, ToDo-Editor, Zeiterfassungs-Timeline mit Edit/Delete/Nachtrag | @@ -19,11 +19,11 @@ Internes Web-Tool von **ToolCTRL** für den täglichen Überblick über alle Pro **Querschnittsfunktionen** -- **Zeiterfassung**: Persistente Timer-Pill in der TopNav (Live-Sekundenzähler), Quick-Picker mit ToDos + Kategorien + Freitext, retroaktiver Nachtrag-Dialog, Bindung an ToDo optional. Personenübergreifend sichtbar auf dem Puls. +- **Zeiterfassung**: Persistente Timer-Pill in der TopNav (Live-Sekundenzähler), Quick-Picker mit ToDos + Kategorien + Freitext, retroaktiver Nachtrag-Dialog, Bindung an ToDo optional. Personenübergreifend sichtbar im Dashboard. - **Blocker**: Inline in der Reflexion oder über `n`-Shortcut bzw. den globalen „Neuer Blocker"-Dialog. Auflösung nur durch den Projekt-P1. - **Pflicht-Minimum-Check** (Leitfaden §9): 6-Punkte-Checkliste pro Projekt, einer davon (Status-Ampel aktuell) wird stündlich per Cron aus den Reflexions-Zeitstempeln aktualisiert. - **Streak**: aufeinanderfolgende Werktage mit voller Reflexion. Flammen-Pill rechts oben. -- **Keyboard-Shortcuts**: `g p/b/h/a` Navigation, `g e/v/m` direkt in Entwicklung/Vertrieb/Marketing, `n` Neuer Blocker, `?` Cheatsheet. +- **Keyboard-Shortcuts**: `g d/b/h/a` Navigation, `g e/v/m` direkt in Entwicklung/Vertrieb/Marketing, `n` Neuer Blocker, `?` Cheatsheet. --- @@ -67,7 +67,7 @@ Internes Web-Tool von **ToolCTRL** für den täglichen Überblick über alle Pro │ │ │ ├── hooks/ useCurrentUser │ │ │ ├── lib/ router, trpc, trpc-provider, voice, │ │ │ │ format-duration, time-categories -│ │ │ ├── routes/ puls, bereich, projekt, heute, reflexion, +│ │ │ ├── routes/ dashboard, bereich, projekt, heute, reflexion, │ │ │ │ archiv, login, not-found │ │ │ └── styles/globals.css │ │ ├── index.html @@ -80,7 +80,7 @@ Internes Web-Tool von **ToolCTRL** für den täglichen Überblick über alle Pro │ ├── cron/routes.ts /internal/cron/* (Bearer-Token geschützt) │ ├── lib/ werktag (Mo-Fr Berlin), ampel, pflicht-minimum, logger │ ├── routers/ _app, user, area, project, deal, gtm, -│ │ campaign, blocker, day-entry, time, puls +│ │ campaign, blocker, day-entry, time, dashboard │ ├── env.ts Zod-validierte Env-Vars │ ├── db.ts Singleton-Drizzle-Client │ ├── trpc.ts Context, protectedProcedure, withAudit @@ -89,7 +89,7 @@ Internes Web-Tool von **ToolCTRL** für den täglichen Überblick über alle Pro ├── packages/ │ ├── db/ Drizzle-Schema + Migrationen │ │ ├── src/{schema,client,seed,index}.ts -│ │ ├── migrations/ 0000_init … 0006_time_entry +│ │ ├── migrations/ 0000_init … 0007_project_description │ │ └── drizzle.config.ts │ └── shared/ Zod-Schemas + Konstanten + Types │ └── src/schemas/ day-entry, project, deal, campaign, time-entry @@ -169,7 +169,7 @@ pnpm --filter @cockpit/db migrate:generate --name= # neue Migration Postgres 16, Drizzle als ORM und Migrator. **Schema-Single-Source-of-Truth**: `packages/db/src/schema.ts`. -### Tabellen (aktueller Stand nach Migration 0006) +### Tabellen (aktueller Stand nach Migration 0007) | Tabelle | Zweck | |---|---| @@ -177,7 +177,7 @@ Postgres 16, Drizzle als ORM und Migrator. **Schema-Single-Source-of-Truth**: `p | `session` | Server-Side Sessions (Postgres-persistent), Browser kennt nur Cookie-`id` | | `audit_event` | Audit-Trail für jede mutierende Operation | | `area` | Bereich (entwicklung / vertrieb / marketing / allgemein) | -| `project` | Projekt mit Cockpit-Sicht-Feldern (Outline-/Plane-URLs, Pflicht-Minimum-State JSONB, etc.) | +| `project` | Projekt: Name, optionale `description`, Outline-/Plane-URLs, Arbeitsmodus (cycle/kanban), `pflicht_minimum_state` JSONB, Status-/Trend-Cache, Archiv-Felder | | `milestone` | Cockpit-Sicht-Meilenstein pro Projekt | | `day_entry` | Tageseintrag (begun / full / absent), `published_at` für Vortrags-Veröffentlichung | | `todo` | Tagesplan-Item, optional an Bezug gebunden, `carried_over`-Flag | @@ -221,7 +221,7 @@ dayEntry today, todayTeam, pendingCarryOver, checkIn, updateTodos, submitReflection, feed, todayPresence time now, start, stop, listToday, update, upsertManual, delete, teamToday -puls overview +dashboard overview ``` Frontend importiert den Typ via `import type { AppRouter } from '@cockpit/api/router'` (siehe `apps/web/src/lib/trpc.ts`). @@ -259,7 +259,7 @@ Im `NODE_ENV=development` ist `/api/auth/dev-login` aktiv. Die Frontend-Login-Ro - Sucht den User mit `email = 'dejan@tool-ctrl.de'`. - Existiert er nicht, wird er via `upsertUserFromOidc` mit `sub='local-dejan'` angelegt. -- Session wird gesetzt, Redirect auf `/puls`. +- Session wird gesetzt, Redirect auf `/dashboard`. In Production ist diese Route nicht registriert, weil die `if (env.NODE_ENV === 'development')`-Guard in `apps/api/src/auth/routes.ts` dann false ist. @@ -299,7 +299,7 @@ Login-Route ist außerhalb der `AppShell` (`/login`). Alle übrigen Routen unter In `apps/web/src/components/layout/AppShell.tsx`: ``` -g p Puls +g d Dashboard g b Bereich (Default Entwicklung) g e Entwicklung g v Vertrieb diff --git a/apps/api/src/auth/routes.ts b/apps/api/src/auth/routes.ts index eec19f6..0c6b16f 100644 --- a/apps/api/src/auth/routes.ts +++ b/apps/api/src/auth/routes.ts @@ -56,8 +56,8 @@ export async function registerAuthRoutes(app: FastifyInstance): Promise { app.get('/api/auth/login', async (req, reply) => { const returnTo = typeof req.query === 'object' && req.query !== null && 'returnTo' in req.query - ? String((req.query as Record).returnTo ?? '/puls') - : '/puls'; + ? String((req.query as Record).returnTo ?? '/dashboard') + : '/dashboard'; const { url, pending } = await buildAuthorizationUrl(returnTo); setPendingCookie(reply, pending); @@ -90,7 +90,7 @@ export async function registerAuthRoutes(app: FastifyInstance): Promise { setSessionCookie(reply, newSession.id); reply.clearCookie(PENDING_COOKIE, { path: '/' }); - const target = pending.returnTo.startsWith('/') ? pending.returnTo : '/puls'; + const target = pending.returnTo.startsWith('/') ? pending.returnTo : '/dashboard'; return reply.redirect(`${env.PUBLIC_BASE_URL}${target}`, 302); } catch (err) { logger.error({ err }, 'oidc callback failed'); @@ -133,7 +133,7 @@ export async function registerAuthRoutes(app: FastifyInstance): Promise { userAgent: req.headers['user-agent'], }); setSessionCookie(reply, newSession.id); - return reply.redirect(`${env.PUBLIC_BASE_URL}/puls`, 302); + return reply.redirect(`${env.PUBLIC_BASE_URL}/dashboard`, 302); }; app.get('/api/auth/dev-login', handler); app.post('/api/auth/dev-login', handler); diff --git a/apps/api/src/routers/_app.ts b/apps/api/src/routers/_app.ts index 2c0b10c..f664335 100644 --- a/apps/api/src/routers/_app.ts +++ b/apps/api/src/routers/_app.ts @@ -3,7 +3,7 @@ import { router, publicProcedure } from '../trpc.js'; import { dayEntryRouter } from './day-entry.js'; import { userRouter } from './user.js'; -import { pulsRouter } from './puls.js'; +import { dashboardRouter } from './dashboard.js'; import { projectRouter } from './project.js'; import { dealRouter, gtmRouter } from './deal.js'; import { campaignRouter } from './campaign.js'; @@ -25,7 +25,7 @@ export const appRouter = router({ blocker: blockerRouter, dayEntry: dayEntryRouter, time: timeRouter, - puls: pulsRouter, + dashboard: dashboardRouter, }); export type AppRouter = typeof appRouter; diff --git a/apps/api/src/routers/puls.ts b/apps/api/src/routers/dashboard.ts similarity index 98% rename from apps/api/src/routers/puls.ts rename to apps/api/src/routers/dashboard.ts index 7767c24..9f779d9 100644 --- a/apps/api/src/routers/puls.ts +++ b/apps/api/src/routers/dashboard.ts @@ -4,7 +4,7 @@ import { router, protectedProcedure } from '../trpc.js'; import { todayIso, previousWerktag } from '../lib/werktag.js'; import { aggregateAreaAmpel } from '../lib/ampel.js'; -export const pulsRouter = router({ +export const dashboardRouter = router({ overview: protectedProcedure.query(async ({ ctx }) => { const todayDate = todayIso(); const [areas, allProjects, openBlockers, todayPresence, totalActiveUsers, feedRows] = diff --git a/apps/api/src/routers/day-entry.ts b/apps/api/src/routers/day-entry.ts index d0c163f..60fd73b 100644 --- a/apps/api/src/routers/day-entry.ts +++ b/apps/api/src/routers/day-entry.ts @@ -297,7 +297,7 @@ export const dayEntryRouter = router({ /** * Konsolidierter Vortrags-Feed: alle bereits publizierten Einträge. - * Nutzbar in Story 3.4 (Puls-Feed). + * Nutzbar in Story 3.4 (Dashboard-Feed). */ feed: protectedProcedure.query(async ({ ctx }) => { const entries = await ctx.db diff --git a/apps/api/src/routers/project.ts b/apps/api/src/routers/project.ts index 40a06da..7bb084a 100644 --- a/apps/api/src/routers/project.ts +++ b/apps/api/src/routers/project.ts @@ -64,6 +64,7 @@ export const projectRouter = router({ .values({ areaId: input.areaId, name: input.name, + description: input.description ?? null, kind: input.kind, verantwortlicheUserId: input.verantwortlicheUserId ?? null, arbeitsmodus: input.arbeitsmodus ?? null, diff --git a/apps/web/src/components/blocker/BlockerDialog.tsx b/apps/web/src/components/blocker/BlockerDialog.tsx index 3b53de6..d018c1c 100644 --- a/apps/web/src/components/blocker/BlockerDialog.tsx +++ b/apps/web/src/components/blocker/BlockerDialog.tsx @@ -11,7 +11,7 @@ export function BlockerDialog({ onClose }: { onClose: () => void }) { const mutation = trpc.blocker.create.useMutation({ onSuccess: async () => { toast(voice.toast.blockerOpened); - await utils.puls.overview.invalidate(); + await utils.dashboard.overview.invalidate(); onClose(); }, }); diff --git a/apps/web/src/components/layout/AppShell.tsx b/apps/web/src/components/layout/AppShell.tsx index 36f56dc..2eb85ca 100644 --- a/apps/web/src/components/layout/AppShell.tsx +++ b/apps/web/src/components/layout/AppShell.tsx @@ -5,7 +5,7 @@ import { useCurrentUser } from '../../hooks/useCurrentUser'; import { BlockerDialog } from '../blocker/BlockerDialog'; const SHORTCUT_MAP: Record = { - p: '/puls', + p: '/dashboard', h: '/heute', b: '/bereich', e: '/bereich/entwicklung', @@ -57,7 +57,7 @@ export function AppShell() { if (e.key === '?' && !waitingForSecond) { // eslint-disable-next-line no-alert alert( - 'Shortcuts:\n g p — Puls\n g b — Bereich\n g e/v/m — Entwicklung/Vertrieb/Marketing\n g h — Heute\n g a — Archiv\n n — Neuer Blocker\n ? — diese Hilfe', + 'Shortcuts:\n g d — Dashboard\n g b — Bereich\n g e/v/m — Entwicklung/Vertrieb/Marketing\n g h — Heute\n g a — Archiv\n n — Neuer Blocker\n ? — diese Hilfe', ); return; } diff --git a/apps/web/src/components/layout/TopNav.tsx b/apps/web/src/components/layout/TopNav.tsx index aeba064..2e6159d 100644 --- a/apps/web/src/components/layout/TopNav.tsx +++ b/apps/web/src/components/layout/TopNav.tsx @@ -7,7 +7,7 @@ import { voice } from '../../lib/voice'; import { useCurrentUser } from '../../hooks/useCurrentUser'; const TABS = [ - { to: '/puls', label: 'Puls', shortcut: 'g p', matchPrefix: '/puls' }, + { to: '/dashboard', label: 'Dashboard', shortcut: 'g d', matchPrefix: '/dashboard' }, { to: '/bereich', label: 'Bereich', shortcut: 'g b', matchPrefix: '/bereich' }, { to: '/heute', label: 'Heute', shortcut: 'g h', matchPrefix: '/heute' }, { to: '/archiv', label: 'Archiv', shortcut: 'g a', matchPrefix: '/archiv' }, @@ -35,7 +35,7 @@ export function TopNav() { className="mx-auto flex h-14 max-w-[1280px] items-center gap-2 px-6" aria-label="Hauptnavigation" > - + {voice.appName} diff --git a/apps/web/src/lib/router.tsx b/apps/web/src/lib/router.tsx index fd6c958..351f695 100644 --- a/apps/web/src/lib/router.tsx +++ b/apps/web/src/lib/router.tsx @@ -3,7 +3,7 @@ import { lazy, Suspense } from 'react'; import { AppShell } from '../components/layout/AppShell'; import { SkeletonCard } from '../components/ui/SkeletonCard'; -const Puls = lazy(() => import('../routes/puls').then((m) => ({ default: m.PulsRoute }))); +const Dashboard = lazy(() => import('../routes/dashboard').then((m) => ({ default: m.DashboardRoute }))); const Heute = lazy(() => import('../routes/heute').then((m) => ({ default: m.HeuteRoute })), ); @@ -33,8 +33,8 @@ const router = createBrowserRouter([ path: '/', element: , children: [ - { index: true, element: withSuspense() }, - { path: 'puls', element: withSuspense() }, + { index: true, element: withSuspense() }, + { path: 'dashboard', element: withSuspense() }, { path: 'heute', element: withSuspense() }, { path: 'reflexion', element: withSuspense() }, { path: 'bereich', element: withSuspense() }, diff --git a/apps/web/src/routes/bereich.tsx b/apps/web/src/routes/bereich.tsx index 6f6b847..46fe89f 100644 --- a/apps/web/src/routes/bereich.tsx +++ b/apps/web/src/routes/bereich.tsx @@ -159,6 +159,7 @@ function ProjectCreateDialog({ }) { const defaultArea = areas.find((a) => a.slug === defaultSlug); const [name, setName] = useState(''); + const [description, setDescription] = useState(''); const [areaId, setAreaId] = useState(defaultArea?.id ?? ''); const [kind, setKind] = useState<'kundenprojekt' | 'internes_projekt'>('internes_projekt'); const [arbeitsmodus, setArbeitsmodus] = useState<'cycle' | 'kanban'>('kanban'); @@ -170,7 +171,7 @@ function ProjectCreateDialog({ onSuccess: async () => { toast(voice.toast.projectCreated); await utils.project.byAreaSlug.invalidate(); - await utils.puls.overview.invalidate(); + await utils.dashboard.overview.invalidate(); onClose(); }, }); @@ -181,6 +182,7 @@ function ProjectCreateDialog({ create.mutate({ areaId, name, + description: description.trim() || undefined, kind, arbeitsmodus, outlineSteckbriefUrl: steckbrief, @@ -207,6 +209,16 @@ function ProjectCreateDialog({ className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm" /> + +