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
@@ -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/<slug>`) | 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=<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
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "project" ADD COLUMN "description" text;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -50,6 +50,13 @@
|
||||
"when": 1780410287425,
|
||||
"tag": "0006_time_entry",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "7",
|
||||
"when": 1780415208906,
|
||||
"tag": "0007_project_description",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -124,6 +124,7 @@ export const project = pgTable(
|
||||
.notNull()
|
||||
.references(() => area.id, { onDelete: 'restrict' }),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
kind: text('kind', { enum: ['kundenprojekt', 'internes_projekt'] }).notNull(),
|
||||
verantwortlicheUserId: uuid('verantwortliche_user_id').references(() => user.id, {
|
||||
onDelete: 'set null',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
||||
export const createProjectInput = z.object({
|
||||
areaId: z.string().uuid(),
|
||||
name: z.string().min(1).max(200),
|
||||
description: z.string().max(2000).optional(),
|
||||
kind: z.enum(['kundenprojekt', 'internes_projekt']),
|
||||
verantwortlicheUserId: z.string().uuid().optional(),
|
||||
arbeitsmodus: z.enum(['cycle', 'kanban']).optional(),
|
||||
|
||||
Reference in new issue
Block a user