Revert "Remove non-billable data from statistics"

This reverts commit 971d493015.
This commit is contained in:
2026-08-05 22:38:14 +02:00
parent bc184fef56
commit 7d4d373863
4 changed files with 75 additions and 28 deletions
+25 -7
View File
@@ -218,7 +218,7 @@ export const helpSections: HelpSection[] = [
{
id: "statistiken",
title: "Statistiken",
description: "Statistiken vergleichen abgerechnete Session-Zeit, offene Bewertungen und Teamspace-Werte.",
description: "Statistiken vergleichen abgerechnete Session-Zeit, nicht abrechenbare Arbeit, offene Bewertungen und Teamspace-Werte.",
quickItems: [
"Abgerechnet und Teamspace haben getrennte Farben.",
"Positive oder gleiche Differenz ist grün.",
@@ -231,8 +231,8 @@ export const helpSections: HelpSection[] = [
"Teamspace zeigt, wie viel Zeit für dieselben abrechenbaren Ticket-Tage im CRM eingetragen wurde.",
"Eine positive Differenz bedeutet: In Teamspace steht mehr Zeit als abgerechnet wurde.",
"Eine negative Differenz bedeutet: In Teamspace steht weniger Zeit als abgerechnet wurde.",
"Nicht abrechenbar wird in den Statistiken nicht angezeigt.",
"Offen wird nur als Prüfwert angezeigt und nicht in Relationen, Graphen, Teamspace-Differenzen, Hotspots, Manuell/Fix-Werte oder Durchschnittswerte eingerechnet.",
"Nicht abrechenbar und Offen werden jeweils als eigene Werte ausgewiesen.",
"Sie werden nicht in Relationen, Graphen, Teamspace-Differenzen, Hotspots, Manuell/Fix-Werte oder Durchschnittswerte eingerechnet.",
"Der Monatsverlauf zeigt abgerechnete Session-Zeit als Balken und Teamspace als Linie.",
"Organisationen, Typen und Ticket-Hotspots helfen dabei, Auffälligkeiten im Monat schnell zu finden."
]
@@ -272,6 +272,23 @@ export const helpSections: HelpSection[] = [
"Fehlende Teamspace-Werte können im Monatsabschluss geprüft und bei Bedarf zur Kenntnis genommen werden."
]
},
{
id: "statistik-bewertet",
title: "Statistik: Nicht abrechenbar",
description: "Diese Kennzahl zeigt Arbeit, die bewusst nicht abgerechnet wird.",
quickItems: [
"Nur Sessions mit Bewertung Nicht abrechenbar.",
"Wird nur separat ausgewiesen.",
"Nicht abrechenbare Sessions blockieren den Monatsabschluss nicht."
],
details: [
"Nicht abrechenbar ist getrennt von Abgerechnet, damit interne, Kulanz- oder nicht fakturierbare Arbeit sichtbar bleibt.",
"Diese Minuten werden nicht mit Teamspace verglichen.",
"Die Zeit zeigt die Summe der nicht abrechenbaren Session-Minuten.",
"Es werden daraus keine Anteile, Differenzen oder Vergleichswerte berechnet.",
"Wenn eine Bewertung zurückgenommen wird, wandert die Session wieder in Offen."
]
},
{
id: "statistik-offen",
title: "Statistik: Offen",
@@ -301,8 +318,8 @@ export const helpSections: HelpSection[] = [
details: [
"Der Monatsverlauf zeigt jeden Tag des Monats. Tage ohne Sessions bleiben leer, damit die Monatsstruktur sichtbar bleibt.",
"Die blauen Balken stehen für abgerechnete Session-Zeit.",
"Die violette Linie steht für Teamspace-Zeit, sofern für den Tag Werte gepflegt wurden.",
"Der Bewertungsstand weist Abgerechnet und Offen als getrennte Werte aus.",
"Die grüne Linie steht für Teamspace-Zeit, sofern für den Tag Werte gepflegt wurden.",
"Der Bewertungsstand weist Abgerechnet, Nicht abrechenbar und Offen nur als getrennte Werte aus.",
"Der Teamspace-Abgleich zeigt die Gesamtdifferenz zwischen CRM-Werten und abgerechneter Session-Zeit.",
"Manuell und Fix trennen selbst erfasste abrechenbare Sessions von automatisch erzeugten abrechenbaren Sessions aus fixen Abrechnungen."
]
@@ -321,6 +338,7 @@ export const helpSections: HelpSection[] = [
"Sessions zeigt die Anzahl der einzelnen Arbeitseinträge.",
"Zeiten vergleicht abgerechnete Session-Zeit mit Teamspace-Zeit.",
"Die Differenz wird als Teamspace minus abgerechnete Zeit berechnet.",
"Nicht abrechenbare Zeit wird separat ausgewiesen und nicht in die Differenz gerechnet.",
"Der Anteil am Monat zeigt, wie groß die abgerechnete Zeit dieser Organisation im Verhältnis zur größten Organisation im Monat ist.",
"Bei vielen Organisationen wird die Liste paginiert, damit die Seite kompakt bleibt."
]
@@ -339,7 +357,7 @@ export const helpSections: HelpSection[] = [
"Support und Consulting werden getrennt summiert.",
"Die Zeitwerte zeigen abgerechnete Sessions und Teamspace für den jeweiligen Typ.",
"Die Differenz folgt derselben Logik wie überall: Teamspace minus abgerechnete Session-Zeit.",
"Die Zeile Abr. und Offen zeigt, wie viele Sessions dieses Typs bereits abgerechnet oder noch offen sind."
"Die Zeile Abr., Nicht und Offen zeigt, wie viele Sessions dieses Typs bereits bewertet oder noch offen sind."
]
},
{
@@ -354,7 +372,7 @@ export const helpSections: HelpSection[] = [
details: [
"Ticket-Hotspots helfen, die größten Aufwandstreiber im Monat schnell zu erkennen.",
"Die Liste zeigt die wichtigsten Tickets nach abgerechneter Zeit.",
"Pro Ticket siehst du Organisation, Sessionanzahl, aktive abrechenbare Tage, abgerechnete Zeit, Teamspace-Zeit und Differenz.",
"Pro Ticket siehst du Organisation, Sessionanzahl, aktive abrechenbare Tage, abgerechnete Zeit, nicht abrechenbare Zeit, Teamspace-Zeit und Differenz.",
"Offene Tickets werden mit einem Warnstatus angezeigt, damit Bewertungen nachgezogen werden können.",
"Über Öffnen gelangst du direkt in die Ticketdetailansicht des Monats."
]
+36 -7
View File
@@ -4,6 +4,7 @@ import {
CircleAlert,
Clock3,
ExternalLink,
ListChecks,
RotateCcw,
Ticket,
TrendingUp,
@@ -30,11 +31,12 @@ type StatisticsPageProps = {
};
const trackedTextClass = "text-sky-700 dark:text-sky-300";
const teamspaceTextClass = "text-violet-700 dark:text-violet-300";
const teamspaceTextClass = "text-emerald-700 dark:text-emerald-300";
const trackedFillClass = "fill-sky-500/70 dark:fill-sky-400/65";
const teamspaceStrokeClass = "text-violet-600 dark:text-violet-300";
const teamspaceStrokeClass = "text-emerald-600 dark:text-emerald-300";
const trackedDotClass = "bg-sky-500 dark:bg-sky-400";
const teamspaceDotClass = "bg-violet-500 dark:bg-violet-400";
const teamspaceDotClass = "bg-emerald-500 dark:bg-emerald-400";
const nonBillableTextClass = "text-indigo-700 dark:text-indigo-300";
const openTextClass = "text-amber-700 dark:text-amber-300";
const organizationPageSize = 8;
@@ -123,6 +125,10 @@ function ColorLegend() {
<span className="size-2 rounded-full bg-red-500 dark:bg-red-400" />
<span className="text-red-700 dark:text-red-300">Differenz minus</span>
</span>
<span className="inline-flex items-center gap-1.5">
<span className="size-2 rounded-full bg-indigo-500 dark:bg-indigo-400" />
<span className={nonBillableTextClass}>nicht abrechenbar</span>
</span>
<span className="inline-flex items-center gap-1.5">
<span className="size-2 rounded-full bg-amber-500 dark:bg-amber-400" />
<span className={openTextClass}>offen</span>
@@ -189,8 +195,10 @@ function buildMonthSeries(month: string, buckets: StatisticsDailyBucket[]) {
sessions: bucket?.sessions ?? 0,
totalMinutes: bucket?.total_minutes ?? 0,
billedMinutes: bucket?.billed_minutes ?? 0,
nonBillableMinutes: bucket?.non_billable_minutes ?? 0,
openMinutes: bucket?.open_minutes ?? 0,
billedSessions: bucket?.billed_sessions ?? 0,
nonBillableSessions: bucket?.non_billable_sessions ?? 0,
openSessions: bucket?.open_sessions ?? 0,
crmBilledMinutes: bucket?.crm_billed_minutes ?? 0
};
@@ -337,6 +345,7 @@ function TicketHotspot({ ticket, month, onNavigate }: { ticket: StatisticsTicket
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs">
<span className={trackedTextClass}>Abgerechnet {formatMinutes(ticket.total_minutes)}</span>
<span className={teamspaceTextClass}>Teamspace {formatMinutes(ticket.crm_billed_minutes)}</span>
<span className={nonBillableTextClass}>Nicht {formatMinutes(ticket.non_billable_minutes)}</span>
<span className="text-muted-foreground">{ticket.billed_sessions} abgerechnet</span>
<span>{ticket.active_days} Tag(e)</span>
<span className={deltaTextClass(ticket.crm_delta_minutes)}>{formatTeamspaceDelta(ticket.crm_delta_minutes)}</span>
@@ -413,6 +422,8 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
const totals = stats?.totals;
const billedSessions = totals?.billedSessions ?? 0;
const billedMinutes = totals?.billedMinutes ?? 0;
const nonBillableSessions = totals?.nonBillableSessions ?? 0;
const nonBillableMinutes = totals?.nonBillableMinutes ?? 0;
const openSessions = totals?.openSessions ?? 0;
const openMinutes = totals?.openMinutes ?? 0;
const crmCoverage = percentage(totals?.crmBilledMinutes ?? 0, totals?.minutes ?? 0);
@@ -460,7 +471,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<MetricCard
label="Abgerechnet"
value={formatMinutes(totals?.minutes ?? 0)}
@@ -479,6 +490,20 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
helpAnchor="statistik-teamspace"
helpLabel="Hilfe zur Kennzahl Teamspace"
/>
<MetricCard
label="Nicht abrechenbar"
value={formatMinutes(nonBillableMinutes)}
detail={
<div className="space-y-0.5">
<div>{nonBillableSessions} Session(s)</div>
<div>wird nicht kalkuliert</div>
</div>
}
icon={ListChecks}
valueClassName={nonBillableTextClass}
helpAnchor="statistik-bewertet"
helpLabel="Hilfe zur Kennzahl Nicht abrechenbar"
/>
<MetricCard
label="Offen"
value={formatMinutes(openMinutes)}
@@ -519,8 +544,9 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<span className="text-muted-foreground">Bewertungsstand</span>
<span className="font-medium">{billedSessions} abrechenbar</span>
</div>
<div className="grid gap-2 text-xs sm:grid-cols-2 xl:grid-cols-1 2xl:grid-cols-2">
<div className="grid gap-2 text-xs sm:grid-cols-3 xl:grid-cols-1 2xl:grid-cols-3">
<div className={trackedTextClass}>Abgerechnet: {billedSessions} Session(s), {formatMinutes(billedMinutes)}</div>
<div className={nonBillableTextClass}>Nicht: {nonBillableSessions} Session(s), {formatMinutes(nonBillableMinutes)}</div>
<div className={openTextClass}>Offen: {openSessions} Session(s), {formatMinutes(openMinutes)}</div>
</div>
</div>
@@ -564,6 +590,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<p className="flex flex-wrap gap-x-3 gap-y-1 text-xs">
<span className={trackedTextClass}>Abgerechnet {formatMinutes(topOrganization.total_minutes)}</span>
<span className={teamspaceTextClass}>Teamspace {formatMinutes(topOrganization.crm_billed_minutes)}</span>
<span className={nonBillableTextClass}>Nicht {formatMinutes(topOrganization.non_billable_minutes)}</span>
</p>
</div>
) : null}
@@ -623,6 +650,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs">
<span className={trackedTextClass}>Abgerechnet {formatMinutes(organization.total_minutes)}</span>
<span className={teamspaceTextClass}>Teamspace {formatMinutes(organization.crm_billed_minutes)}</span>
<span className={nonBillableTextClass}>Nicht {formatMinutes(organization.non_billable_minutes)}</span>
<span className="text-muted-foreground">{percentage(organization.total_minutes, totals?.minutes ?? 0)}% vom Monat</span>
<span className={deltaTextClass(organization.crm_delta_minutes)}>{formatTeamspaceDelta(organization.crm_delta_minutes)}</span>
</div>
@@ -693,8 +721,9 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<span className={trackedTextClass}>Abgerechnet {formatMinutes(group.total_minutes)}</span>
<span className={teamspaceTextClass}>Teamspace {formatMinutes(group.crm_billed_minutes)}</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs text-muted-foreground">
<div className="grid grid-cols-3 gap-2 text-xs text-muted-foreground">
<span>Abr. {group.billed_sessions} / {formatMinutes(group.billed_minutes)}</span>
<span>Nicht {group.non_billable_sessions} / {formatMinutes(group.non_billable_minutes)}</span>
<span>Offen {group.open_sessions} / {formatMinutes(group.open_minutes)}</span>
</div>
</div>
@@ -732,7 +761,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<Ticket className="size-8 text-muted-foreground" />
<div>
<p className="font-medium">Keine abgerechneten Sessions in diesem Monat</p>
<p className="text-sm text-muted-foreground">Nur abgerechnete Sessions fließen in Statistiken und Vergleiche ein.</p>
<p className="text-sm text-muted-foreground">Offene oder nicht abrechenbare Sessions werden separat gezeigt und fließen nicht in Statistiken oder Vergleiche ein.</p>
</div>
<Button variant="secondary" size="sm" onClick={() => setMonth(currentMonth())}>
<RotateCcw className="size-4" />