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
211 lines
6.9 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|