From 6a81258ca0e03fade7b16c4f8d557363ae1c09b1 Mon Sep 17 00:00:00 2001 From: Sascha Wolf Date: Tue, 18 Aug 2026 15:32:03 +0200 Subject: [PATCH] fix(dashboard): Tagesachse lag einen Tag daneben, Bewerbungszahl erklaert Zwei Sachen, die zusammen wie ein Rechenfehler aussahen: - Bei der Einteilung "Tag" baute die Achse ihre Schluessel mit toISOString(). Das rechnet nach UTC und liefert fuer den lokalen Tagesanfang in Zeitzonen oestlich von Greenwich den Vortag - jede Saeule fiel neben die Achse und das Diagramm blieb leer, obwohl die Kennzahlen Werte zeigten. Die Schluessel kommen jetzt aus den Ortsdatumsteilen. Monat, Quartal und Jahr waren nie betroffen, die haben ihre Schluessel schon so gebaut. - Die Kennzahl "Bewerbungen" zaehlt alle Akquise-Eintraege des Zeitraums, auch die, aus denen eine Opportunity geworden ist. Die stehen nicht mehr in der Akquise-Liste, also ist die Zahl regelmaessig hoeher als die Liste lang ist. Das stand nirgends: jetzt sagt die Fussnote der Kachel, wie viele uebernommen wurden, und unter den Kacheln steht ein Satz dazu. Dazu passen bei 30 Tagen jetzt alle Abschnitte nebeneinander (schmalere Spalten), und passt es doch nicht, zeigt das Diagramm das Ende des Zeitraums - die aktuellen Tage stehen rechts und interessieren. Co-Authored-By: Claude Opus 5 (1M context) --- docs/07-anleitung-benutzer.md | 6 ++-- src/components/dashboard/Dashboard.tsx | 45 +++++++++++++++++++++--- src/components/dashboard/dashboardUi.tsx | 15 ++++++-- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/docs/07-anleitung-benutzer.md b/docs/07-anleitung-benutzer.md index c52fc2a..90d5bec 100644 --- a/docs/07-anleitung-benutzer.md +++ b/docs/07-anleitung-benutzer.md @@ -96,7 +96,7 @@ geschnitten wird - Tag, Monat, Quartal oder Jahr. | Kachel | Was gezählt wird | | --- | --- | -| **Bewerbungen** | Einträge in der Akquise im Zeitraum. Jeder Eintrag ist eine versendete Bewerbung | +| **Bewerbungen** | Einträge in der Akquise mit Funddatum im Zeitraum. Jeder Eintrag ist eine versendete Bewerbung - **auch die, aus denen inzwischen eine Opportunity geworden ist**. Die Zahl ist deshalb regelmäßig höher als die Akquise-Liste lang ist; unter den Kacheln steht, wie viele übernommen wurden. Ein von Hand gelöschter Eintrag zählt nicht mehr mit | | **Zu Opportunity** | Bewerbungen, aus denen eine Opportunity geworden ist - dazu die Quote | | **Gewonnen** | Opportunities, die in die Phase *Beauftragt* gekommen sind | | **Beim Kunden** | Opportunities, die **jetzt** in *Kundenvorstellung* oder *Interview Kunde* stehen - unabhängig vom Zeitraum | @@ -106,7 +106,9 @@ geschnitten wird - Tag, Monat, Quartal oder Jahr. Das Säulendiagramm zeigt dieselben drei Reihen über der Zeit: Bewerbungen, daraus entstandene Opportunities und gewonnene Projekte. Leere Abschnitte bleiben sichtbar - eine Woche ohne Bewerbung ist eine Aussage. Der Mauszeiger auf einer -Säule nennt Abschnitt, Reihe und Wert. +Säule nennt Abschnitt, Reihe und Wert. Passen nicht alle Abschnitte nebeneinander, +zeigt das Diagramm das **Ende** des Zeitraums und lässt sich nach links scrollen - +die aktuellen Tage stehen rechts. Rechts daneben steht **Bewerbungen je Berater** für denselben Zeitraum. Einträge ohne hinterlegten Berater erscheinen als *Ohne Berater*. diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx index ff478f0..4bb105e 100644 --- a/src/components/dashboard/Dashboard.tsx +++ b/src/components/dashboard/Dashboard.tsx @@ -42,7 +42,20 @@ const BUCKETS: { id: DashboardBucket; label: string }[] = [ { id: 'jahr', label: 'Jahr' }, ]; -const iso = (date: Date) => date.toISOString().slice(0, 10); +/** + * Ortsdatum als `JJJJ-MM-TT`. + * + * Bewusst nicht `toISOString()`: das rechnet nach UTC und liefert in Zeitzonen + * oestlich von Greenwich fuer den lokalen Tagesanfang den Vortag. Die + * Tagesschluessel liefen damit einen Tag hinter denen der Datenbank her - die + * Saeulen fielen neben die Achse und das Diagramm blieb leer, obwohl die + * Kennzahlen Werte zeigten. + */ +function iso(date: Date): string { + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${date.getFullYear()}-${month}-${day}`; +} /** Anfang und Ende einer Vorgabe - `to` ist immer heute. */ function rangeDates(range: (typeof RANGES)[number]): { from: string; to: string } { @@ -295,15 +308,19 @@ export const Dashboard: React.FC = ({ opportunities, events, isL value={totals.applications} accent="border-l-sky-500" icon="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" - hint="Einträge in der Akquise im gewählten Zeitraum" - footnote={`versendet · ${periodLabel}`} + hint="Alle Akquise-Einträge mit Funddatum im Zeitraum - auch die, aus denen inzwischen eine Opportunity geworden ist und die deshalb nicht mehr in der Akquise-Liste stehen." + footnote={ + totals.converted > 0 + ? `versendet · ${totals.converted} davon übernommen` + : `versendet · ${periodLabel}` + } /> = ({ opportunities, events, isL /> + {/* + * Erklaerung direkt unter den Kacheln: die Zahl der Bewerbungen ist + * regelmaessig hoeher als die Akquise-Liste lang ist, weil uebernommene + * Eintraege dort verschwinden. Ohne diesen Satz sieht das nach einem + * Fehler aus. + */} +

+ Bewerbungen zählt alle + Akquise-Einträge mit Funddatum im Zeitraum + {totals.converted > 0 && ( + <> + {' '} + - davon sind {totals.converted} zu + einer Opportunity geworden und stehen deshalb nicht mehr in der Akquise-Liste + + )} + . Ein von Hand gelöschter Eintrag zählt nicht mehr mit - ein Fehleintrag soll die + Zahlen nicht verfälschen. +

+ {/* Verlauf und Berater */}
= ({ points }) => { const [grown, setGrown] = useState(false); + const scrollRef = useRef(null); useEffect(() => { setGrown(false); @@ -137,6 +138,16 @@ export const SeriesChart: React.FC<{ points: SeriesPoint[] }> = ({ points }) => return () => clearTimeout(timer); }, [points]); + /* + * Passen nicht alle Abschnitte nebeneinander, wird das Ende gezeigt: bei 30 + * Tagen stehen die aktuellen Tage rechts, und die interessieren. Ohne das + * sieht man den Anfang des Zeitraums - haeufig lauter leere Tage. + */ + useEffect(() => { + const box = scrollRef.current; + if (box) box.scrollLeft = box.scrollWidth; + }, [points]); + const max = Math.max(1, ...points.flatMap((p) => [p.applications, p.converted, p.won])); // Runde Obergrenze, damit die Hilfslinien lesbare Werte tragen. const step = Math.max(1, Math.ceil(max / 4)); @@ -153,8 +164,8 @@ export const SeriesChart: React.FC<{ points: SeriesPoint[] }> = ({ points }) => ))}
-
-
+
+
{/* Werteachse */}
{[4, 3, 2, 1, 0].map((line) => (