Puls → Dashboard umbenennen + Projekt-Beschreibung
ci / quality (push) Has been cancelled
ci / build (push) Has been cancelled

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) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 4.7 committed 2026-06-02 17:51:08 +02:00
1 parent 4879556905
commit c8fc024ca0
21 files changed
+2025 -39

No files matched your search

@@ -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();
},
});
+2 -2
View File
@@ -5,7 +5,7 @@ import { useCurrentUser } from '../../hooks/useCurrentUser';
import { BlockerDialog } from '../blocker/BlockerDialog';
const SHORTCUT_MAP: Record<string, string> = {
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;
}
+2 -2
View File
@@ -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"
>
<NavLink to="/puls" className="flex items-center gap-1.5 font-bold text-[var(--color-ink)]">
<NavLink to="/dashboard" className="flex items-center gap-1.5 font-bold text-[var(--color-ink)]">
<span>{voice.appName}</span>
<ToolCtrlLogo size={18} />
</NavLink>
+3 -3
View File
@@ -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: <AppShell />,
children: [
{ index: true, element: withSuspense(<Puls />) },
{ path: 'puls', element: withSuspense(<Puls />) },
{ index: true, element: withSuspense(<Dashboard />) },
{ path: 'dashboard', element: withSuspense(<Dashboard />) },
{ path: 'heute', element: withSuspense(<Heute />) },
{ path: 'reflexion', element: withSuspense(<Reflexion />) },
{ path: 'bereich', element: withSuspense(<Bereich />) },
+13 -1
View File
@@ -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"
/>
</Field>
<Field label="Beschreibung (optional)">
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
maxLength={2000}
rows={3}
placeholder="Worum geht's? Kurz beschreiben."
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
/>
</Field>
<Field label="Bereich">
<select
required
@@ -9,8 +9,8 @@ import { clsx } from 'clsx';
type Ampel = 'green' | 'amber' | 'red' | 'grey';
export function PulsRoute() {
const q = trpc.puls.overview.useQuery();
export function DashboardRoute() {
const q = trpc.dashboard.overview.useQuery();
const today = trpc.dayEntry.today.useQuery();
if (q.isPending) {
@@ -31,7 +31,7 @@ export function PulsRoute() {
<div className="space-y-8">
<header className="flex items-end justify-between">
<div>
<h1 className="text-3xl font-bold">Unternehmens-Puls</h1>
<h1 className="text-3xl font-bold">Dashboard</h1>
<p className="text-[var(--color-ink-muted)]">
Stand: {formatDate(q.data.date)} · Jourfixe {q.data.jourfixe.label}
</p>
+1 -1
View File
@@ -29,7 +29,7 @@ export function HeuteRoute() {
return <EinstempeltView todos={today.data.todos} entry={today.data.entry} />;
}
return <CheckInForm onDone={() => navigate('/puls')} />;
return <CheckInForm onDone={() => navigate('/dashboard')} />;
}
function CheckInForm({ onDone }: { onDone: () => void }) {
+3 -3
View File
@@ -5,13 +5,13 @@ export function NotFoundRoute() {
<div className="mx-auto mt-16 max-w-md text-center">
<h1 className="text-3xl font-bold">Nicht gefunden.</h1>
<p className="mt-2 text-[var(--color-ink-muted)]">
Diese Seite gibt es nicht. Vielleicht meintest du den Puls?
Diese Seite gibt es nicht. Vielleicht meintest du das Dashboard?
</p>
<Link
to="/puls"
to="/dashboard"
className="mt-6 inline-flex h-9 items-center rounded-[var(--radius-md)] bg-[var(--color-brand)] px-4 text-sm font-medium text-[var(--color-brand-fg)] hover:bg-black/85"
>
Zum Puls
Zum Dashboard
</Link>
</div>
);
+8 -3
View File
@@ -48,8 +48,8 @@ export function ProjektRoute() {
<ArrowLeft size={16} />
</button>
<p className="text-sm text-[var(--color-ink-muted)]">
<Link to="/puls" className="hover:underline">
Puls
<Link to="/dashboard" className="hover:underline">
Dashboard
</Link>
{' › '}
<Link to={`/bereich/${a.slug}`} className="hover:underline">
@@ -72,6 +72,11 @@ export function ProjektRoute() {
{p.kind === 'kundenprojekt' ? 'Kundenprojekt' : 'Internes Projekt'} ·{' '}
{p.arbeitsmodus ?? '—'} · {p.deadline ? `Deadline ${p.deadline}` : 'kein Deadline'}
</p>
{p.description && (
<p className="mt-3 max-w-2xl whitespace-pre-wrap text-sm text-[var(--color-ink)]">
{p.description}
</p>
)}
</header>
{abschlussOpen && (
@@ -171,7 +176,7 @@ function AbschlussDialog({ projectId, onClose }: { projectId: string; onClose: (
toast(voice.toast.projectArchived);
await utils.project.byId.invalidate({ id: projectId });
await utils.project.listArchived.invalidate();
await utils.puls.overview.invalidate();
await utils.dashboard.overview.invalidate();
navigate('/archiv');
},
});
+1 -1
View File
@@ -34,7 +34,7 @@ export function ReflexionRoute() {
<ReflectForm
entry={today.data.entry}
todos={today.data.todos}
onDone={() => navigate('/puls')}
onDone={() => navigate('/dashboard')}
/>
);
}