fix: Bug-Review-Fixes + README aktualisiert
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:
1 parent
19db774c4c
commit
22f79460b2
12 files changed
+110
-79
No files matched your search
@@ -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)}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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' && (
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in new issue
Block a user