467 Stellen. Nicht durch einen anderen Strich ersetzt, sondern durch das
Satzzeichen, das an der jeweiligen Stelle richtig ist: Doppelpunkt, wo eine
Erlaeuterung folgt; Komma, wo der Satz weiterlaeuft; Punkt, wo zwei
Hauptsaetze aneinanderstiessen. Ein Halbgeviertstrich waere nur derselbe
Griff mit kuerzerem Strich gewesen.
Regelbasiert ersetzt und danach nachgeprueft, weil eine Regel allein nicht
reicht. Gefunden und einzeln korrigiert:
- Eingeschobene Saetze mit Strichpaar. Nur der oeffnende Strich passte ins
Muster, der schliessende blieb als "—," stehen. Drei Stellen, jetzt mit
Klammern oder als eigener Satz.
- Doppelte Doppelpunkte, wo beide Striche eines Paars zu Doppelpunkten wurden
("Globale Regeln: Schrift, Groessen, Abstaende: kommen aus HiveGrid").
Sieben Stellen in Doku und Oberflaeche, jetzt in Klammern.
- Pfade, denen ein Komma statt eines Doppelpunkts folgte.
- Ausgerichtete Kommentartabellen, in denen der Strich eine Spalte war.
Nicht angefasst: drei Geviertstriche, die keine Prosa sind, sondern
Tabellenplatzhalter fuer "kein Wert" (Trichter, Auftraege, Kunden). Dort ist
der Strich die Angabe selbst; ihn zu entfernen liesse leere Zellen zurueck.
tsc, lint, build, verify:carbon und verify:layout laufen sauber.
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 { 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>
|
|
|
|
<div className="grid grid-cols-1 items-start gap-4 lg:grid-cols-2">
|
|
<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>
|
|
</div>
|
|
|
|
<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>
|
|
</>
|
|
);
|
|
}
|