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>
39 lines
1.4 KiB
TypeScript
39 lines
1.4 KiB
TypeScript
// 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 aktualisiert.
|
||
|
||
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: 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) {
|
||
// Jüngsten Statusbericht DIESES Projekts heranziehen (statt global).
|
||
const last = (
|
||
await db
|
||
.select({ createdAt: projectStatus.createdAt })
|
||
.from(projectStatus)
|
||
.where(eq(projectStatus.projectId, p.id))
|
||
.orderBy(desc(projectStatus.createdAt))
|
||
.limit(1)
|
||
)[0];
|
||
|
||
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)) {
|
||
await db.update(project).set({ pflichtMinimumState: newState }).where(eq(project.id, p.id));
|
||
updated++;
|
||
}
|
||
}
|
||
return { updated };
|
||
}
|