Puls → Dashboard umbenennen + Projekt-Beschreibung
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:
1 parent
4879556905
commit
c8fc024ca0
21 files changed
+2025
-39
No files matched your search
@@ -56,8 +56,8 @@ export async function registerAuthRoutes(app: FastifyInstance): Promise<void> {
|
||||
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<string, unknown>).returnTo ?? '/puls')
|
||||
: '/puls';
|
||||
? String((req.query as Record<string, unknown>).returnTo ?? '/dashboard')
|
||||
: '/dashboard';
|
||||
|
||||
const { url, pending } = await buildAuthorizationUrl(returnTo);
|
||||
setPendingCookie(reply, pending);
|
||||
@@ -90,7 +90,7 @@ export async function registerAuthRoutes(app: FastifyInstance): Promise<void> {
|
||||
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<void> {
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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] =
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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 />) },
|
||||
|
||||
@@ -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>
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ export function ReflexionRoute() {
|
||||
<ReflectForm
|
||||
entry={today.data.entry}
|
||||
todos={today.data.todos}
|
||||
onDone={() => navigate('/puls')}
|
||||
onDone={() => navigate('/dashboard')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user