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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6f57483f33
commit
6a81258ca0
@@ -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<DashboardProps> = ({ 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}`
|
||||
}
|
||||
/>
|
||||
<StatCard
|
||||
label="Zu Opportunity"
|
||||
value={totals.converted}
|
||||
accent="border-l-violet-500"
|
||||
icon="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
|
||||
hint="Bewerbungen, aus denen eine Opportunity geworden ist"
|
||||
hint="Bewerbungen, aus denen eine Opportunity geworden ist. Diese Einträge stehen nicht mehr in der Akquise-Liste - sie leben als Opportunity weiter."
|
||||
footnote={quote(totals.converted, totals.applications)}
|
||||
/>
|
||||
<StatCard
|
||||
@@ -324,6 +341,26 @@ export const Dashboard: React.FC<DashboardProps> = ({ opportunities, events, isL
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
* 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.
|
||||
*/}
|
||||
<p className="px-1 text-xs text-ink-400">
|
||||
<strong className="font-bold text-ink-500">Bewerbungen</strong> zählt alle
|
||||
Akquise-Einträge mit Funddatum im Zeitraum
|
||||
{totals.converted > 0 && (
|
||||
<>
|
||||
{' '}
|
||||
- davon sind <strong className="font-bold text-ink-500">{totals.converted}</strong> 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.
|
||||
</p>
|
||||
|
||||
{/* Verlauf und Berater */}
|
||||
<div className="grid gap-5 xl:grid-cols-[2fr_1fr]">
|
||||
<Panel
|
||||
|
||||
@@ -130,6 +130,7 @@ export const SERIES = [
|
||||
*/
|
||||
export const SeriesChart: React.FC<{ points: SeriesPoint[] }> = ({ points }) => {
|
||||
const [grown, setGrown] = useState(false);
|
||||
const scrollRef = useRef<HTMLDivElement>(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 }) =>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="scrollbar-slim overflow-x-auto">
|
||||
<div className="flex" style={{ minWidth: `${Math.max(560, points.length * 74)}px` }}>
|
||||
<div ref={scrollRef} className="scrollbar-slim overflow-x-auto">
|
||||
<div className="flex" style={{ minWidth: `${Math.max(520, points.length * 46)}px` }}>
|
||||
{/* Werteachse */}
|
||||
<div className="relative w-10 shrink-0" style={{ height: '260px' }}>
|
||||
{[4, 3, 2, 1, 0].map((line) => (
|
||||
|
||||
Reference in New Issue
Block a user