diff --git a/backend/src/index.ts b/backend/src/index.ts index 37f3756..8c4080d 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -768,7 +768,7 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) { ` ${baseCte} SELECT - COUNT(DISTINCT ticket_id) FILTER (WHERE billing_status = 'billed')::int AS tickets, + COUNT(DISTINCT ticket_id)::int AS tickets, COUNT(*)::int AS sessions, COALESCE(SUM(rounded_minutes) FILTER (WHERE billing_status = 'billed'), 0)::int AS minutes, COALESCE(SUM(rounded_minutes) FILTER (WHERE billing_status = 'billed'), 0)::int AS billed_minutes, @@ -832,8 +832,8 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) { SELECT organization_id::text AS organization_id, organization_name, - COUNT(DISTINCT ticket_id) FILTER (WHERE total_minutes > 0)::int AS tickets, - COALESCE(SUM(billed_sessions), 0)::int AS sessions, + COUNT(DISTINCT ticket_id)::int AS tickets, + SUM(session_count)::int AS sessions, COALESCE(SUM(total_minutes), 0)::int AS total_minutes, COALESCE(SUM(COALESCE(billed_minutes, 0)), 0)::int AS billed_minutes, COALESCE(SUM(COALESCE(non_billable_minutes, 0)), 0)::int AS non_billable_minutes, @@ -844,7 +844,6 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) { COALESCE(SUM(crm_billed_minutes), 0)::int AS crm_billed_minutes FROM session_group_with_crm GROUP BY organization_id, organization_name - HAVING COALESCE(SUM(total_minutes), 0) > 0 ORDER BY total_minutes DESC, organization_name ASC; `, [period.startIso, period.endIso, userId] @@ -854,8 +853,8 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) { ${baseCte} SELECT work_type, - COUNT(DISTINCT ticket_id) FILTER (WHERE total_minutes > 0)::int AS tickets, - COALESCE(SUM(billed_sessions), 0)::int AS sessions, + COUNT(DISTINCT ticket_id)::int AS tickets, + SUM(session_count)::int AS sessions, COALESCE(SUM(total_minutes), 0)::int AS total_minutes, COALESCE(SUM(COALESCE(billed_minutes, 0)), 0)::int AS billed_minutes, COALESCE(SUM(COALESCE(non_billable_minutes, 0)), 0)::int AS non_billable_minutes, @@ -866,7 +865,6 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) { COALESCE(SUM(crm_billed_minutes), 0)::int AS crm_billed_minutes FROM session_group_with_crm GROUP BY work_type - HAVING COALESCE(SUM(total_minutes), 0) > 0 ORDER BY total_minutes DESC, work_type ASC; `, [period.startIso, period.endIso, userId] diff --git a/docs/user-guide.md b/docs/user-guide.md index 0cb693a..1cf3639 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -326,7 +326,7 @@ Fix erzeugte einzelne Sessions koennen geloescht werden, zum Beispiel wegen Urla ## Statistiken -Die Seite `Statistiken` betrachtet immer einen Monat. Sie dient als Ueberblick fuer abgerechnete Zeiten, Teamspace-Vergleich, offene Bewertungen und Auffaelligkeiten. +Die Seite `Statistiken` betrachtet immer einen Monat. Sie dient als Ueberblick fuer abgerechnete Zeiten, nicht abrechenbare Arbeit, Teamspace-Vergleich, Bewertungsstand und Auffaelligkeiten. ### Kennzahlen @@ -349,14 +349,15 @@ Das bedeutet: Farben: - Abgerechnet: blau -- Teamspace: violett +- Teamspace: gruen - Positive oder ausgeglichene Differenz: gruen - Negative Differenz: rot +- Nicht abrechenbar: indigo - Offen: gelb/orange -Nicht abrechenbare Sessions werden in den Statistiken nicht angezeigt. +`Nicht abrechenbar` zeigt Sessions, die bewusst nicht fakturiert werden. Diese Minuten werden nicht mit Teamspace verglichen. -`Offen` zeigt Sessions ohne Bewertung als reinen Pruefwert. Offene Zeiten werden nicht in Statistik- oder Vergleichswerte eingerechnet und blockieren den Monatsabschluss. +`Offen` zeigt Sessions ohne Bewertung. Diese blockieren den Monatsabschluss. ### Monatsverlauf und Abrechnungsqualitaet @@ -364,7 +365,7 @@ Diese Card kombiniert den Tagesverlauf und die Bewertungsqualitaet. - Balken zeigen abgerechnete Session-Zeit. - Die Linie zeigt Teamspace-Zeit. -- Der Bewertungsstand trennt abgerechnete und offene Sessions. +- Der Bewertungsstand trennt abgerechnete, nicht abrechenbare und offene Sessions. - Der Teamspace-Abgleich zeigt die Gesamtdifferenz. - Manuell und Fix trennen manuell erfasste abrechenbare Sessions von automatisch erzeugten abrechenbaren Sessions. @@ -376,7 +377,7 @@ Sichtbar sind: - Organisation - Sessions -- Zeiten: Abgerechnet und Teamspace +- Zeiten: Abgerechnet, nicht abrechenbar und Teamspace - Differenz - Anteil an der abgerechneten Monatszeit @@ -391,9 +392,10 @@ Sie zeigt je Typ: - Tickets - Sessions - Abgerechnete Session-Zeit +- Nicht abrechenbare Session-Zeit - Teamspace-Zeit - Differenz -- Anzahl abgerechnet und offen +- Anzahl abgerechnet, nicht abrechenbar und offen ### Ticket-Hotspots diff --git a/frontend/src/help-content.ts b/frontend/src/help-content.ts index 21ffa79..a61d5eb 100644 --- a/frontend/src/help-content.ts +++ b/frontend/src/help-content.ts @@ -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." ] diff --git a/frontend/src/views/StatisticsPage.tsx b/frontend/src/views/StatisticsPage.tsx index 6dc7d24..3d8fba8 100644 --- a/frontend/src/views/StatisticsPage.tsx +++ b/frontend/src/views/StatisticsPage.tsx @@ -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() { Differenz minus + + + nicht abrechenbar + offen @@ -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
Abgerechnet {formatMinutes(ticket.total_minutes)} Teamspace {formatMinutes(ticket.crm_billed_minutes)} + Nicht {formatMinutes(ticket.non_billable_minutes)} {ticket.billed_sessions} abgerechnet {ticket.active_days} Tag(e) {formatTeamspaceDelta(ticket.crm_delta_minutes)} @@ -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) {
-
+
+ +
{nonBillableSessions} Session(s)
+
wird nicht kalkuliert
+
+ } + icon={ListChecks} + valueClassName={nonBillableTextClass} + helpAnchor="statistik-bewertet" + helpLabel="Hilfe zur Kennzahl Nicht abrechenbar" + /> Bewertungsstand {billedSessions} abrechenbar
-
+
Abgerechnet: {billedSessions} Session(s), {formatMinutes(billedMinutes)}
+
Nicht: {nonBillableSessions} Session(s), {formatMinutes(nonBillableMinutes)}
Offen: {openSessions} Session(s), {formatMinutes(openMinutes)}
@@ -564,6 +590,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {

Abgerechnet {formatMinutes(topOrganization.total_minutes)} Teamspace {formatMinutes(topOrganization.crm_billed_minutes)} + Nicht {formatMinutes(topOrganization.non_billable_minutes)}

) : null} @@ -623,6 +650,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
Abgerechnet {formatMinutes(organization.total_minutes)} Teamspace {formatMinutes(organization.crm_billed_minutes)} + Nicht {formatMinutes(organization.non_billable_minutes)} {percentage(organization.total_minutes, totals?.minutes ?? 0)}% vom Monat {formatTeamspaceDelta(organization.crm_delta_minutes)}
@@ -693,8 +721,9 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) { Abgerechnet {formatMinutes(group.total_minutes)} Teamspace {formatMinutes(group.crm_billed_minutes)} -
+
Abr. {group.billed_sessions} / {formatMinutes(group.billed_minutes)} + Nicht {group.non_billable_sessions} / {formatMinutes(group.non_billable_minutes)} Offen {group.open_sessions} / {formatMinutes(group.open_minutes)}
@@ -732,7 +761,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {

Keine abgerechneten Sessions in diesem Monat

-

Nur abgerechnete Sessions fließen in Statistiken und Vergleiche ein.

+

Offene oder nicht abrechenbare Sessions werden separat gezeigt und fließen nicht in Statistiken oder Vergleiche ein.