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
@@ -96,7 +96,7 @@ geschnitten wird - Tag, Monat, Quartal oder Jahr.
|
|||||||
|
|
||||||
| Kachel | Was gezählt wird |
|
| 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 |
|
| **Zu Opportunity** | Bewerbungen, aus denen eine Opportunity geworden ist - dazu die Quote |
|
||||||
| **Gewonnen** | Opportunities, die in die Phase *Beauftragt* gekommen sind |
|
| **Gewonnen** | Opportunities, die in die Phase *Beauftragt* gekommen sind |
|
||||||
| **Beim Kunden** | Opportunities, die **jetzt** in *Kundenvorstellung* oder *Interview Kunde* stehen - unabhängig vom Zeitraum |
|
| **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
|
Das Säulendiagramm zeigt dieselben drei Reihen über der Zeit: Bewerbungen, daraus
|
||||||
entstandene Opportunities und gewonnene Projekte. Leere Abschnitte bleiben
|
entstandene Opportunities und gewonnene Projekte. Leere Abschnitte bleiben
|
||||||
sichtbar - eine Woche ohne Bewerbung ist eine Aussage. Der Mauszeiger auf einer
|
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
|
Rechts daneben steht **Bewerbungen je Berater** für denselben Zeitraum. Einträge
|
||||||
ohne hinterlegten Berater erscheinen als *Ohne Berater*.
|
ohne hinterlegten Berater erscheinen als *Ohne Berater*.
|
||||||
|
|||||||
@@ -42,7 +42,20 @@ const BUCKETS: { id: DashboardBucket; label: string }[] = [
|
|||||||
{ id: 'jahr', label: 'Jahr' },
|
{ 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. */
|
/** Anfang und Ende einer Vorgabe - `to` ist immer heute. */
|
||||||
function rangeDates(range: (typeof RANGES)[number]): { from: string; to: string } {
|
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}
|
value={totals.applications}
|
||||||
accent="border-l-sky-500"
|
accent="border-l-sky-500"
|
||||||
icon="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
|
icon="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
|
||||||
hint="Einträge in der Akquise im gewählten Zeitraum"
|
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={`versendet · ${periodLabel}`}
|
footnote={
|
||||||
|
totals.converted > 0
|
||||||
|
? `versendet · ${totals.converted} davon übernommen`
|
||||||
|
: `versendet · ${periodLabel}`
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Zu Opportunity"
|
label="Zu Opportunity"
|
||||||
value={totals.converted}
|
value={totals.converted}
|
||||||
accent="border-l-violet-500"
|
accent="border-l-violet-500"
|
||||||
icon="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
|
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)}
|
footnote={quote(totals.converted, totals.applications)}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
@@ -324,6 +341,26 @@ export const Dashboard: React.FC<DashboardProps> = ({ opportunities, events, isL
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 */}
|
{/* Verlauf und Berater */}
|
||||||
<div className="grid gap-5 xl:grid-cols-[2fr_1fr]">
|
<div className="grid gap-5 xl:grid-cols-[2fr_1fr]">
|
||||||
<Panel
|
<Panel
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ export const SERIES = [
|
|||||||
*/
|
*/
|
||||||
export const SeriesChart: React.FC<{ points: SeriesPoint[] }> = ({ points }) => {
|
export const SeriesChart: React.FC<{ points: SeriesPoint[] }> = ({ points }) => {
|
||||||
const [grown, setGrown] = useState(false);
|
const [grown, setGrown] = useState(false);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setGrown(false);
|
setGrown(false);
|
||||||
@@ -137,6 +138,16 @@ export const SeriesChart: React.FC<{ points: SeriesPoint[] }> = ({ points }) =>
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [points]);
|
}, [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]));
|
const max = Math.max(1, ...points.flatMap((p) => [p.applications, p.converted, p.won]));
|
||||||
// Runde Obergrenze, damit die Hilfslinien lesbare Werte tragen.
|
// Runde Obergrenze, damit die Hilfslinien lesbare Werte tragen.
|
||||||
const step = Math.max(1, Math.ceil(max / 4));
|
const step = Math.max(1, Math.ceil(max / 4));
|
||||||
@@ -153,8 +164,8 @@ export const SeriesChart: React.FC<{ points: SeriesPoint[] }> = ({ points }) =>
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="scrollbar-slim overflow-x-auto">
|
<div ref={scrollRef} className="scrollbar-slim overflow-x-auto">
|
||||||
<div className="flex" style={{ minWidth: `${Math.max(560, points.length * 74)}px` }}>
|
<div className="flex" style={{ minWidth: `${Math.max(520, points.length * 46)}px` }}>
|
||||||
{/* Werteachse */}
|
{/* Werteachse */}
|
||||||
<div className="relative w-10 shrink-0" style={{ height: '260px' }}>
|
<div className="relative w-10 shrink-0" style={{ height: '260px' }}>
|
||||||
{[4, 3, 2, 1, 0].map((line) => (
|
{[4, 3, 2, 1, 0].map((line) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user