Aus dem Sparring, alle drei auch in Analytics wirksam. 1. Vertriebstrichter: Zeilen-Hervorhebung statt Ausgrauen der uebrigen Balken. Bisher setzte das Ueberfahren einer Zeile alle anderen Balken auf 45 % Deckkraft. Das war aus drei Gruenden falsch: Es liess fuenf von sechs Balken verblassen und las sich dadurch wie eine Auswahl statt wie ein Ueberfahren; es war in sich widerspruechlich, weil die Balken verblassten und die Namen daneben schwarz blieben; und Deckkraft traegt in diesem Diagramm bereits Bedeutung — der Balken kodiert einen Anteil, ihn heller zu machen heisst dort "weniger" und nicht "gerade nicht beruehrt". Jetzt bekommt die beruehrte Zeile einen Untergrund, die uebrigen bleiben unveraendert. Derselbe Untergrund wie in der Handlungsbedarf-Liste und in den Tabellen. Die Spur des aktiven Balkens wird weiss: neutral-50 und neutral-100 liegen nur fuenf Stufen auseinander, grau auf grau haette dem Balken den Bezugsrahmen genommen. 2. KPI-Karten einer Reihe auf gleiche Hoehe. Die Umsatzkarte war hoeher als ihre drei Nachbarn, weil sie als einzige eine Trendzeile mit Verlaufskurve traegt. Auf dem Dashboard steht zwischen Rasterzelle und Karte ein Link, und der reichte die Streckung des Rasters nicht durch. h-full auf der Karte. 3. Handlungsbedarf: Hover-Flaeche greift ueber den Inhalt hinaus. Der Untergrund endete hart am Pfeil — bei zwei Spalten besonders sichtbar, weil daneben 32 px Spaltenabstand ungefaerbt blieben. Jetzt acht Pixel Ueberstand und runde Ecken. Dabei mitgenommen: Der Trennstrich sass unten und brauchte eine Ausnahme fuer "die letzte Zeile" — die es bei zwei Spalten zweimal gibt. Die rechte Spalte behielt dadurch einen Strich, unter dem nichts mehr kam. Oben liegend ist die Ausnahme die erste Zeile jeder Spalte, und die steht immer an derselben Stelle, unabhaengig von der Anzahl der Eintraege. tsc, lint, build und verify:layout laufen sauber; Hover-Zustaende und Kartenhoehen nachgemessen statt begutachtet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
136 lines
5.5 KiB
TypeScript
136 lines
5.5 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import {
|
|
FeatherAlertTriangle,
|
|
FeatherArrowRight,
|
|
FeatherCheckCircle,
|
|
FeatherInfo,
|
|
} from "@subframe/core";
|
|
import { STATUS_FARBEN } from "../../utils/chartTheme";
|
|
import { formatEuro } from "../../utils/formatters";
|
|
import { useToast } from "../../context/ToastContext";
|
|
import type { AlertEintrag } from "../../data/calculations";
|
|
|
|
/**
|
|
* Handlungsbedarf-Liste (COMP-08).
|
|
*
|
|
* Jede Zeile nennt drei Dinge: was los ist, um wie viel Geld es geht und was
|
|
* zu tun ist. Ohne den Betrag lassen sich die Zeilen nicht gegeneinander
|
|
* abwägen, ohne die Handlung ist die Liste ein Bericht statt einer Arbeitsliste.
|
|
* Der Absprung in die jeweilige Arbeitsliste ist im Prototyp eine Attrappe.
|
|
*/
|
|
export function AlertList({
|
|
eintraege,
|
|
leerText = "Keine offenen Punkte",
|
|
/** Zwei Spalten, wenn die Karte über die volle Seitenbreite läuft. */
|
|
spalten = 1,
|
|
}: {
|
|
eintraege: AlertEintrag[];
|
|
leerText?: string;
|
|
spalten?: 1 | 2;
|
|
}) {
|
|
const { zeigeToast } = useToast();
|
|
const rang = { kritisch: 0, warnung: 1, hinweis: 2 } as const;
|
|
const sortiert = [...eintraege].sort((a, b) => rang[a.art] - rang[b.art]);
|
|
|
|
if (!sortiert.length) {
|
|
return (
|
|
<div className="flex h-full min-h-[180px] flex-col items-center justify-center gap-2 text-center">
|
|
<FeatherCheckCircle className="text-[22px]" style={{ color: STATUS_FARBEN.positiv }} />
|
|
<span className="text-[13px] text-text-neutral-tertiary">{leerText}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
// Container-Query statt Viewport: dieselbe Liste steht mal über die volle
|
|
// Seitenbreite und mal in einer Drittelkarte. Der Viewport weiß davon nichts.
|
|
<ul
|
|
className={`@container ${
|
|
spalten === 2 ? "grid grid-cols-1 gap-x-8 xl:grid-cols-2" : "flex flex-col"
|
|
}`}
|
|
>
|
|
{sortiert.map((eintrag) => {
|
|
const farbe =
|
|
eintrag.art === "kritisch"
|
|
? STATUS_FARBEN.negativ
|
|
: eintrag.art === "warnung"
|
|
? STATUS_FARBEN.warnung
|
|
: STATUS_FARBEN.info;
|
|
|
|
return (
|
|
/**
|
|
* Der Trennstrich sitzt oben, nicht unten.
|
|
*
|
|
* Unten liegend braucht er eine Ausnahme für die letzte Zeile — und
|
|
* „die letzte Zeile" gibt es bei zwei Spalten zweimal, einmal je
|
|
* Spalte, an einer Stelle, die von der Anzahl der Einträge abhängt.
|
|
* Die rechte Spalte behielt dadurch einen Strich, unter dem nichts
|
|
* mehr kam.
|
|
*
|
|
* Oben liegend ist die Ausnahme dagegen die *erste* Zeile jeder
|
|
* Spalte, und die steht bei zeilenweisem Fluss immer an derselben
|
|
* Stelle: die ersten beiden Kinder. Das gilt unabhängig davon, wie
|
|
* viele Einträge die Liste hat.
|
|
*/
|
|
<li
|
|
key={eintrag.id}
|
|
className={`border-t border-solid border-border-default first:border-t-0 ${
|
|
spalten === 2 ? "xl:[&:nth-child(-n+2)]:border-t-0" : ""
|
|
}`}
|
|
>
|
|
<button
|
|
onClick={() =>
|
|
zeigeToast(eintrag.aktion, `${eintrag.text} — Absprung folgt in der Umsetzung`)
|
|
}
|
|
/* -mx-2 und die zusätzliche Breite: Der Untergrund lief vorher
|
|
genau bis zum Pfeil und schnitt dort hart ab — bei zwei Spalten
|
|
besonders sichtbar, weil daneben 32 px Spaltenabstand ungefärbt
|
|
blieben. Jetzt greift er acht Pixel über den Inhalt hinaus und
|
|
hat runde Ecken; er liest sich als Fläche unter der Zeile statt
|
|
als Kasten um sie herum.
|
|
|
|
Der Trennstrich sitzt dafür an der Zeile und nicht mehr am
|
|
Knopf: Er soll die Zeilen trennen und nicht mitwandern. */
|
|
className="group -mx-2 flex w-[calc(100%+1rem)] items-center gap-3 rounded-md px-2 py-3 text-left transition-colors duration-150 hover:bg-neutral-50"
|
|
>
|
|
<span className="flex-none" style={{ color: farbe }}>
|
|
{eintrag.art === "hinweis" ? (
|
|
<FeatherInfo className="text-[16px]" />
|
|
) : (
|
|
<FeatherAlertTriangle className="text-[16px]" />
|
|
)}
|
|
</span>
|
|
|
|
<span className="flex min-w-0 grow flex-col">
|
|
<span className="min-w-0 break-words text-[13px] font-medium leading-snug text-text-neutral-primary">
|
|
{eintrag.text}
|
|
</span>
|
|
<span className="min-w-0 break-words text-[12px] leading-snug text-text-neutral-tertiary">
|
|
{eintrag.detail}
|
|
</span>
|
|
</span>
|
|
|
|
{eintrag.betrag !== undefined ? (
|
|
<span className="flex-none whitespace-nowrap text-[14px] font-semibold tabular-nums text-text-neutral-primary">
|
|
{formatEuro(eintrag.betrag)}
|
|
</span>
|
|
) : null}
|
|
|
|
{/* Schmale Karte: nur der Pfeil, die Handlung steht im Tooltip. */}
|
|
<span
|
|
title={eintrag.aktion}
|
|
className="flex flex-none items-center justify-end gap-1 whitespace-nowrap text-[12px] font-medium text-text-neutral-tertiary group-hover:text-text-neutral-primary @md:w-[150px]"
|
|
>
|
|
<span className="hidden @md:inline">{eintrag.aktion}</span>
|
|
<FeatherArrowRight className="text-[13px]" />
|
|
</span>
|
|
</button>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
);
|
|
}
|