Files
dejanandClaude Opus 5 bada5db344 Menue nicht mehr abschneiden, Zeile teilt sich nach dem Ausblenden neu auf
Zwei Fehler aus dem Sparring, beide an der Diagrammkarte.

**Das Optionsmenue war abgeschnitten.** Die Karte trug `overflow-hidden`, das
Menue klappt aus dem Kopf heraus, und der zweite Eintrag war nur zur Haelfte
zu sehen. Das Beschneiden ist entfallen; die runden Ecken brauchen es nicht,
weil Kopf und Koerper 16 px Innenabstand haben und nichts an der Kante anliegt.

**Nach dem Ausblenden blieb eine Luecke.** Die Zeilen waren ein Raster mit
festen Spalten. Blendet man eine Karte aus, gibt ChartCard `null` zurueck, die
Spalte bleibt aber stehen und die Nachbarkarte behaelt ihre zwei Drittel.

Neu ist `ChartReihe` in PageContainer.tsx: eine Flexzeile statt eines Rasters.
Die verbliebenen Karten verteilen den Platz unter sich neu, ohne dass jemand
mitzaehlen muesste, wie viele gerade sichtbar sind. Nachgemessen auf
/analytics: Der Zahlungsverlauf geht von 574 px auf 1164 px, sobald die
Nachbarkachel verschwindet.

Zehn Zeilen auf allen fuenf Analyse-Seiten umgestellt. `gleichHoch` haelt die
Unterscheidung aus dem letzten Sparring fest: gestreckt nur, wo die Karten
dieselbe Bauart haben.

Beim Umbau eine Regression gebaut und durch Nachmessen gefunden: Aus 885/443
wurde 664/664. `[&>*]:flex-1` erzeugt den Selektor `.zeile > *`, und der ist
spezifischer als eine Klasse am Kind, also verlor `xl:flex-[2]`. Jetzt
`xl:flex-[2]!`, mit dem Grund als Kommentar an der Komponente.

Nachgemessen nach der Aenderung, alle Zeilen: 885/443 wo zwei zu eins gewollt
ist, 389/389 und 409/409 und 595/595 wo gleiche Hoehe gewollt ist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
2026-08-31 23:09:39 +02:00

211 lines
6.9 KiB
TypeScript

"use client";
import React from "react";
import { ChartReihe, PageContainer, Raster } from "../components/layout/PageContainer";
import { KPICard } from "../components/cards/KPICard";
import { ChartCard } from "../components/cards/ChartCard";
import { AuslastungsBalken } from "../components/charts/AuslastungsBalken";
import { BalkenHorizontal } from "../components/charts/BalkenHorizontal";
import { Donut } from "../components/charts/Donut";
import { DataTable, type Spalte } from "../components/tables/DataTable";
import { formatEuro, formatEuroKompakt, formatProzent, formatStunden, formatZahl } from "../utils/formatters";
import { SeitenZustand } from "../components/layout/SeitenZustand";
import type { VMState } from "../view-models";
import type { TeamDaten, TeamZeile } from "../view-models/team.vm";
export function TeamSeite({ vm }: { vm: VMState<TeamDaten> }) {
if (vm.status !== "ready") {
return <SeitenZustand vm={vm} />;
}
const d = vm.data;
const spalten: Spalte<TeamZeile>[] = [
{
key: "name",
titel: "Mitarbeiter",
breite: "26%",
zelle: (k) => (
<div className="flex items-center gap-2.5">
<span className="flex h-7 w-7 flex-none items-center justify-center rounded-full bg-neutral-100 text-[11px] font-semibold text-text-neutral-secondary">
{k.mitarbeiter.kuerzel}
</span>
<div className="flex flex-col">
<span className="font-medium">{k.mitarbeiter.name}</span>
<span className="text-[11px] text-text-neutral-tertiary">
{k.mitarbeiter.rolle} · {k.mitarbeiter.gewerk}
</span>
</div>
</div>
),
sortWert: (k) => k.mitarbeiter.name,
},
{
key: "status",
titel: "Status",
zelle: (k) =>
k.mitarbeiter.status === "aktiv" ? (
<span className="rounded-full bg-[#defbe6] px-2 py-0.5 text-[11px] font-medium text-[#0e6027]">
aktiv
</span>
) : (
<span className="rounded-full bg-neutral-100 px-2 py-0.5 text-[11px] font-medium text-neutral-600">
inaktiv
</span>
),
sortWert: (k) => k.mitarbeiter.status,
},
{
key: "stunden",
titel: "Stunden",
numerisch: true,
zelle: (k) => formatStunden(k.stunden),
sortWert: (k) => k.stunden,
},
{
key: "produktiv",
titel: "davon produktiv",
numerisch: true,
zelle: (k) => formatStunden(k.produktivStunden),
sortWert: (k) => k.produktivStunden,
},
{
key: "auslastung",
titel: "Auslastung",
numerisch: true,
zelle: (k) => (
<span
className={
k.auslastungProzent > 95
? "font-medium text-[#8a3800]"
: k.auslastungProzent < 70
? "text-[#00539a]"
: "font-medium text-[#0e6027]"
}
>
{formatProzent(k.auslastungProzent, 0)}
</span>
),
sortWert: (k) => k.auslastungProzent,
},
{
key: "abgeschlossen",
titel: "Abgeschlossen",
numerisch: true,
zelle: (k) => formatZahl(k.abgeschlossen),
sortWert: (k) => k.abgeschlossen,
},
{
key: "umsatz",
titel: "Auftragswert",
numerisch: true,
zelle: (k) => <span className="font-medium">{formatEuro(k.umsatz)}</span>,
sortWert: (k) => k.umsatz,
},
];
return (
<>
<PageContainer>
<Raster spalten={5}>
<KPICard
label="Im Einsatz"
wert={formatZahl(d.aktive.length)}
zusatz={`von ${formatZahl(d.mitarbeiterGesamt)} gewerblich Beschäftigten`}
icon="HardHat"
bezug="stichtag"
/>
<KPICard
label="Ø Auslastung"
wert={formatProzent(d.durchschnittAuslastung, 0)}
zusatz="Produktive Stunden zu verfügbaren"
icon="Activity"
akzent={d.durchschnittAuslastung > 95 ? "warnung" : "positiv"}
/>
<KPICard
label="Produktivquote"
wert={formatProzent(d.produktivQuote, 0)}
zusatz="Anteil auftragsbezogener Stunden"
icon="Target"
/>
<KPICard
label="Erfasste Stunden"
wert={formatStunden(d.stundenGesamt)}
zusatz={`${formatStunden(d.produktivGesamt)} auf Aufträge gebucht`}
icon="Clock"
/>
<KPICard
label="Abwesenheit"
wert={formatStunden(d.abwesenheitStunden)}
zusatz="Urlaub, Krankheit, Fortbildung"
icon="CalendarOff"
richtungGut="negativ"
/>
</Raster>
<ChartCard
id="team-auslastung"
titel="Auslastung je Mitarbeiter"
untertitel="Produktive Stunden gegen vertraglich verfügbare Stunden"
hinweis="Werte über 100 % bedeuten Überstunden. Dauerhaft über 95 % ist ein Warnsignal für Terminrisiken."
>
<AuslastungsBalken
zeilen={d.aktive.map((k) => ({
name: k.mitarbeiter.name,
wert: k.auslastungProzent,
zusatz: `${k.mitarbeiter.rolle} · ${k.mitarbeiter.gewerk}`,
}))}
maxZeilen={12}
/>
</ChartCard>
<ChartReihe umbruch="lg">
<ChartCard
id="team-abschluesse"
titel="Abschlüsse je Mitarbeiter"
untertitel="Anzahl im gewählten Zeitraum, dahinter der zugehörige Auftragswert"
hinweis="Der Balken zeigt die Anzahl, sie ist die vergleichbare Größe. Der Auftragswert steht daneben, ist aber ausdrücklich kein Leistungsvergleich: Ein Wärmepumpen-Auftrag ist um ein Vielfaches größer als eine Wartung."
>
<BalkenHorizontal
daten={d.aktive.map((k) => ({
name: k.mitarbeiter.name,
value: k.abgeschlossen,
zweitwert: k.umsatz,
}))}
formatWert={(w) => formatZahl(w)}
formatZweitwert={formatEuroKompakt}
maxBalken={8}
/>
</ChartCard>
<ChartCard
id="team-abwesenheit"
titel="Abwesenheiten"
untertitel="Stunden im gewählten Zeitraum"
>
<Donut
daten={d.abwesenheitNach}
formatWert={(w) => formatStunden(w)}
hoehe={210}
mitteLabel="Stunden"
/>
</ChartCard>
</ChartReihe>
<ChartCard
id="team-tabelle"
titel="Mitarbeiterübersicht"
untertitel="Alle gewerblich Beschäftigten mit Kennzahlen"
>
<DataTable
zeilen={d.zeilen}
spalten={spalten}
proSeite={10}
suchFelder={(k) => `${k.mitarbeiter.name} ${k.mitarbeiter.rolle} ${k.mitarbeiter.gewerk}`}
/>
</ChartCard>
</PageContainer>
</>
);
}