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

+5 -2
View File
@@ -1,6 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { Play, Square, Plus } from 'lucide-react';
import { clsx } from 'clsx';
import { toast } from 'sonner';
import { trpc } from '../../lib/trpc';
import { formatDuration } from '../../lib/format-duration';
@@ -174,11 +173,15 @@ function QuickPicker({
</div>
<div className="mt-3 border-t border-[var(--color-border)] pt-2">
<label className="block px-1 text-[10px] uppercase tracking-wide text-[var(--color-ink-faint)]">
<label
htmlFor="timer-freetext"
className="block px-1 text-[10px] uppercase tracking-wide text-[var(--color-ink-faint)]"
>
Mit freiem Text
</label>
<div className="mt-1 flex gap-1.5">
<input
id="timer-freetext"
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
+2 -1
View File
@@ -61,7 +61,8 @@ const inputCls =
'w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm';
function today(): string {
return new Date().toISOString().slice(0, 10);
// Berlin-Datum (nicht UTC) — sonst springt der Default abends auf morgen.
return new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Berlin' }).format(new Date());
}
export function LeadRoute() {
+2 -1
View File
@@ -154,7 +154,8 @@ function LeadImportDialog({ onClose }: { onClose: () => void }) {
</label>
{fileName && !result && (
<p className="text-xs text-[var(--color-ink-muted)]">
Bereit: {fileName} ({csv.split('\n').filter((l) => l.trim()).length - 1} Datenzeilen)
Bereit: {fileName} ({Math.max(0, csv.split('\n').filter((l) => l.trim()).length - 1)}{' '}
Datenzeilen)
</p>
)}
+9 -4
View File
@@ -50,6 +50,11 @@ type ProjectDetail = inferRouterOutputs<AppRouter>['project']['byId']['project']
const inputCls =
'w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm';
// Datum in Berlin-Zeit als YYYY-MM-DD (nicht UTC — sonst Off-by-one abends).
function formatDay(d: string | Date): string {
return new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Berlin' }).format(new Date(d));
}
function countSentences(text: string): number {
return text
.split(/[.!?]+/)
@@ -487,7 +492,7 @@ function PhaseSection({ projectId, phase }: { projectId: string; phase: ProjectP
<span>
<span className="font-medium">{PHASE_META[g.phase as ProjectPhase].label}</span>{' '}
<span className="text-xs text-[var(--color-ink-faint)]">
{new Date(g.reviewedAt).toISOString().slice(0, 10)}
{formatDay(g.reviewedAt)}
</span>
{g.notiz && <span className="block text-xs text-[var(--color-ink-muted)]">{g.notiz}</span>}
</span>
@@ -672,7 +677,7 @@ function StatusSection({
<span className="rounded-full bg-black/5 px-2 py-0.5 text-[10px] text-[var(--color-ink-muted)]">extern</span>
)}
<span className="ml-auto text-xs text-[var(--color-ink-faint)]">
{new Date(latest.createdAt).toISOString().slice(0, 10)}
{formatDay(latest.createdAt)}
{latest.authorName ? ` · ${latest.authorName}` : ''}
</span>
</div>
@@ -695,7 +700,7 @@ function StatusSection({
}[s.ampel])} />
<span>
<span className="text-xs text-[var(--color-ink-faint)]">
{new Date(s.createdAt).toISOString().slice(0, 10)}
{formatDay(s.createdAt)}
</span>
{s.text && <span className="block">{s.text}</span>}
</span>
@@ -790,7 +795,7 @@ function ChangeRow({ projectId, change: c }: { projectId: string; change: Change
<p className="font-medium">{c.titel}</p>
{c.auswirkung && <p className="mt-0.5 text-xs text-[var(--color-ink-muted)]">Auswirkung: {c.auswirkung}</p>}
<p className="mt-1 text-xs text-[var(--color-ink-faint)]">
{new Date(c.createdAt).toISOString().slice(0, 10)}
{formatDay(c.createdAt)}
{c.authorName ? ` · ${c.authorName}` : ''}
</p>
{c.entscheidung === 'genehmigt' && (
+7 -5
View File
@@ -93,10 +93,12 @@ function ReflectForm({
}
mutation.mutate({
dayEntryId: entry.id,
todos: Object.entries(todoStates).map(([id, s]) => ({
id,
status: s.status,
dropReason: s.reason,
// Über die aktuelle ToDo-Liste mappen (nicht nur den initialen Snapshot),
// damit mitten am Tag ergänzte ToDos ihren Status behalten.
todos: todos.map((t) => ({
id: t.id,
status: todoStates[t.id]?.status ?? t.status,
dropReason: todoStates[t.id]?.reason,
})),
reflectionText,
ampel,
@@ -125,7 +127,7 @@ function ReflectForm({
</p>
<ul className="space-y-2">
{todos.map((t) => {
const status = todoStates[t.id]?.status ?? 'open';
const status = todoStates[t.id]?.status ?? t.status;
return (
<li key={t.id} className="flex items-center gap-2">
<EditableTodoTitle id={t.id} title={t.title} />