fix: Bug-Review-Fixes + README aktualisiert
ci / quality (push) Failing after 38s
ci / build (push) Has been skipped
deploy / deploy (push) Successful in 53s

Backend:
- Vortags-Feed (dashboard.overview + dayEntry.feed) sortierte aufsteigend
  mit limit(50) → zeigte die ÄLTESTEN statt neuesten Einträge. Jetzt desc.
- deal.updateStage: wonValueEur wurde beim Gewinnen mit der Wahrscheinlichkeit
  gewichtet → zu niedriger Won-Umsatz. Jetzt voller Auftragswert.
- Pflicht-Minimum „Status aktuell" wurde global (letzte Reflexion irgendwo)
  statt pro Projekt berechnet. Jetzt je Projekt aus dem jüngsten Statusbericht.
- time.listToday/teamToday nutzten fixen +02:00-Offset → im Winter (CET) um
  1h verschobene Tagesgrenze. Jetzt DST-sicher via AT TIME ZONE 'Europe/Berlin'.

Frontend:
- lead.tsx + projekt.tsx: Datumsanzeigen nutzten UTC (toISOString) → abends
  Off-by-one. Jetzt Berlin-lokal formatiert.
- reflexion.tsx: Submit mappte nur den initialen ToDo-Snapshot → mitten am Tag
  ergänzte ToDos verloren ihren Status. Jetzt über die aktuelle Liste.
- leads.tsx: CSV-Zeilenzähler konnte „-1" zeigen (Math.max).
- TimerPill: Label ohne Control (a11y-Lint-Error, war repo-weit rot) + unused
  clsx-Import behoben → pnpm lint jetzt 0 Errors.

DB: seed.ts unbedingten updatedAt-Bump aller User entfernt (Debug-Rest).

README: vollständig auf aktuellen Stand gebracht (Leads/CRM, CSV-Import,
PM-Tiefe, Phasen/Status/Risiken/Stakeholder/Änderungen, Tabellen bis 0011,
tRPC-Router, staleTime 0, Auto-Deploy-Runner + Startup-Migrationen).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 4.8 committed 2026-06-06 17:00:03 +02:00
1 parent 19db774c4c
commit 22f79460b2
12 files changed
+110 -79

No files matched your search

+15 -18
View File
@@ -1,32 +1,32 @@
// Pflicht-Minimum-Berechnung (Leitfaden §9).
// Punkte 1–4 + 6 werden manuell in der App gepflegt; Punkt 5 (Status-Ampel aktuell)
// wird hier zeitabhängig berechnet und im Cron auf alle Projekte aktualisiert.
// wird hier zeitabhängig berechnet und im Cron aktualisiert.
import { eq, isNotNull, isNull, and, desc } from 'drizzle-orm';
import { project, dayEntry } from '@cockpit/db';
import { werktageBetween, todayIso } from './werktag.js';
import { eq, desc } from 'drizzle-orm';
import { project, projectStatus } from '@cockpit/db';
import type { Db } from '@cockpit/db';
import { werktageBetween, todayIso, formatIsoDate } from './werktag.js';
const MAX_AGE_WERKTAGE = 7;
export async function recomputePflichtMinimumStatusActual(
db: import('@cockpit/db').Db,
): Promise<{ updated: number }> {
export async function recomputePflichtMinimumStatusActual(db: Db): Promise<{ updated: number }> {
const projects = await db.select().from(project).where(eq(project.archiviert, false));
const today = todayIso();
let updated = 0;
for (const p of projects) {
// Letzte Reflexion mit Projektbezug? (Vereinfachung: letzter day_entry mit Ampel,
// dessen Update das Projekt-Status-Cache gesetzt hat = `status_cache` selbst.)
const lastEntry = (
// Jüngsten Statusbericht DIESES Projekts heranziehen (statt global).
const last = (
await db
.select()
.from(dayEntry)
.where(and(isNotNull(dayEntry.reflectedAt)))
.orderBy(desc(dayEntry.reflectedAt))
.select({ createdAt: projectStatus.createdAt })
.from(projectStatus)
.where(eq(projectStatus.projectId, p.id))
.orderBy(desc(projectStatus.createdAt))
.limit(1)
)[0];
const actual = lastEntry ? werktageBetween(lastEntry.date, today) <= MAX_AGE_WERKTAGE : false;
const actual = last
? werktageBetween(formatIsoDate(last.createdAt), today) <= MAX_AGE_WERKTAGE
: false;
const newState = { ...p.pflichtMinimumState, status_aktuell: actual };
if (JSON.stringify(newState) !== JSON.stringify(p.pflichtMinimumState)) {
@@ -36,6 +36,3 @@ export async function recomputePflichtMinimumStatusActual(
}
return { updated };
}
// linter
void isNull;
+2 -2
View File
@@ -1,4 +1,4 @@
import { and, eq, ne, isNotNull, isNull, count } from 'drizzle-orm';
import { and, eq, ne, isNotNull, isNull, count, desc } from 'drizzle-orm';
import { area, project, blocker, dayEntry, user, risk, type Area, type Project } from '@cockpit/db';
import { router, protectedProcedure } from '../trpc.js';
import { todayIso, previousWerktag } from '../lib/werktag.js';
@@ -31,7 +31,7 @@ export const dashboardRouter = router({
.from(dayEntry)
.innerJoin(user, eq(user.id, dayEntry.userId))
.where(and(isNotNull(dayEntry.publishedAt), eq(dayEntry.status, 'full')))
.orderBy(dayEntry.publishedAt)
.orderBy(desc(dayEntry.publishedAt))
.limit(50),
ctx.db
.select({
+2 -2
View File
@@ -1,5 +1,5 @@
import { TRPCError } from '@trpc/server';
import { and, eq, isNull, isNotNull, lt, asc, inArray } from 'drizzle-orm';
import { and, eq, isNull, isNotNull, lt, asc, desc, inArray } from 'drizzle-orm';
import { dayEntry, todo, blocker, type Todo } from '@cockpit/db';
import { DayEntrySchemas } from '@cockpit/shared';
import { router, protectedProcedure } from '../trpc.js';
@@ -375,7 +375,7 @@ export const dayEntryRouter = router({
.select()
.from(dayEntry)
.where(isNotNull(dayEntry.publishedAt))
.orderBy(dayEntry.publishedAt)
.orderBy(desc(dayEntry.publishedAt))
.limit(50);
return entries;
}),
+2 -3
View File
@@ -64,10 +64,9 @@ export const dealRouter = router({
stage: input.stage,
enteredStageAt: new Date(),
wonAt: input.stage === 'abschluss' ? new Date() : null,
// Gewonnener Deal = voller Auftragswert (nicht mit Wahrscheinlichkeit gewichtet).
wonValueEur:
input.stage === 'abschluss'
? sql`COALESCE(${deal.valueEur} * ${deal.probability} / 100, 0)`
: null,
input.stage === 'abschluss' ? sql`COALESCE(${deal.valueEur}, 0)` : null,
})
.where(eq(deal.id, input.dealId))
.returning();
+5 -10
View File
@@ -1,6 +1,6 @@
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { and, eq, gte, isNull, lte, desc } from 'drizzle-orm';
import { and, eq, isNull, desc, sql } from 'drizzle-orm';
import { timeEntry, dayEntry, type TimeEntry } from '@cockpit/db';
import { TimeEntrySchemas } from '@cockpit/shared';
import { router, protectedProcedure } from '../trpc.js';
@@ -129,16 +129,14 @@ export const timeRouter = router({
*/
listToday: protectedProcedure.query(async ({ ctx }) => {
const today = todayIso();
const startOfDay = new Date(`${today}T00:00:00+02:00`);
const startOfTomorrow = new Date(startOfDay.getTime() + 24 * 60 * 60 * 1000);
const rows = await ctx.db
.select()
.from(timeEntry)
.where(
and(
eq(timeEntry.userId, ctx.user.id),
gte(timeEntry.startedAt, startOfDay),
lte(timeEntry.startedAt, startOfTomorrow),
// Tagesgrenze TZ-korrekt (DST-sicher), statt fixem +02:00-Offset.
sql`(${timeEntry.startedAt} AT TIME ZONE 'Europe/Berlin')::date = ${today}`,
),
)
.orderBy(desc(timeEntry.startedAt));
@@ -243,17 +241,14 @@ export const timeRouter = router({
*/
teamToday: protectedProcedure.query(async ({ ctx }) => {
const today = todayIso();
const startOfDay = new Date(`${today}T00:00:00+02:00`);
const startOfTomorrow = new Date(startOfDay.getTime() + 24 * 60 * 60 * 1000);
const { todo } = await import('@cockpit/db');
const { inArray } = await import('drizzle-orm');
const rows = await ctx.db
.select()
.from(timeEntry)
.where(
and(gte(timeEntry.startedAt, startOfDay), lte(timeEntry.startedAt, startOfTomorrow)),
);
// Tagesgrenze TZ-korrekt (DST-sicher), statt fixem +02:00-Offset.
.where(sql`(${timeEntry.startedAt} AT TIME ZONE 'Europe/Berlin')::date = ${today}`);
const todoIds = Array.from(
new Set(rows.map((r) => r.todoId).filter((x): x is string => Boolean(x))),