Remove non-billable data from statistics
This commit is contained in:
@@ -768,7 +768,7 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) {
|
|||||||
`
|
`
|
||||||
${baseCte}
|
${baseCte}
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(DISTINCT ticket_id)::int AS tickets,
|
COUNT(DISTINCT ticket_id) FILTER (WHERE billing_status = 'billed')::int AS tickets,
|
||||||
COUNT(*)::int AS sessions,
|
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 minutes,
|
||||||
COALESCE(SUM(rounded_minutes) FILTER (WHERE billing_status = 'billed'), 0)::int AS billed_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
|
SELECT
|
||||||
organization_id::text AS organization_id,
|
organization_id::text AS organization_id,
|
||||||
organization_name,
|
organization_name,
|
||||||
COUNT(DISTINCT ticket_id)::int AS tickets,
|
COUNT(DISTINCT ticket_id) FILTER (WHERE total_minutes > 0)::int AS tickets,
|
||||||
SUM(session_count)::int AS sessions,
|
COALESCE(SUM(billed_sessions), 0)::int AS sessions,
|
||||||
COALESCE(SUM(total_minutes), 0)::int AS total_minutes,
|
COALESCE(SUM(total_minutes), 0)::int AS total_minutes,
|
||||||
COALESCE(SUM(COALESCE(billed_minutes, 0)), 0)::int AS billed_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,
|
COALESCE(SUM(COALESCE(non_billable_minutes, 0)), 0)::int AS non_billable_minutes,
|
||||||
@@ -844,6 +844,7 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) {
|
|||||||
COALESCE(SUM(crm_billed_minutes), 0)::int AS crm_billed_minutes
|
COALESCE(SUM(crm_billed_minutes), 0)::int AS crm_billed_minutes
|
||||||
FROM session_group_with_crm
|
FROM session_group_with_crm
|
||||||
GROUP BY organization_id, organization_name
|
GROUP BY organization_id, organization_name
|
||||||
|
HAVING COALESCE(SUM(total_minutes), 0) > 0
|
||||||
ORDER BY total_minutes DESC, organization_name ASC;
|
ORDER BY total_minutes DESC, organization_name ASC;
|
||||||
`,
|
`,
|
||||||
[period.startIso, period.endIso, userId]
|
[period.startIso, period.endIso, userId]
|
||||||
@@ -853,8 +854,8 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) {
|
|||||||
${baseCte}
|
${baseCte}
|
||||||
SELECT
|
SELECT
|
||||||
work_type,
|
work_type,
|
||||||
COUNT(DISTINCT ticket_id)::int AS tickets,
|
COUNT(DISTINCT ticket_id) FILTER (WHERE total_minutes > 0)::int AS tickets,
|
||||||
SUM(session_count)::int AS sessions,
|
COALESCE(SUM(billed_sessions), 0)::int AS sessions,
|
||||||
COALESCE(SUM(total_minutes), 0)::int AS total_minutes,
|
COALESCE(SUM(total_minutes), 0)::int AS total_minutes,
|
||||||
COALESCE(SUM(COALESCE(billed_minutes, 0)), 0)::int AS billed_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,
|
COALESCE(SUM(COALESCE(non_billable_minutes, 0)), 0)::int AS non_billable_minutes,
|
||||||
@@ -865,6 +866,7 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) {
|
|||||||
COALESCE(SUM(crm_billed_minutes), 0)::int AS crm_billed_minutes
|
COALESCE(SUM(crm_billed_minutes), 0)::int AS crm_billed_minutes
|
||||||
FROM session_group_with_crm
|
FROM session_group_with_crm
|
||||||
GROUP BY work_type
|
GROUP BY work_type
|
||||||
|
HAVING COALESCE(SUM(total_minutes), 0) > 0
|
||||||
ORDER BY total_minutes DESC, work_type ASC;
|
ORDER BY total_minutes DESC, work_type ASC;
|
||||||
`,
|
`,
|
||||||
[period.startIso, period.endIso, userId]
|
[period.startIso, period.endIso, userId]
|
||||||
|
|||||||
+7
-9
@@ -326,7 +326,7 @@ Fix erzeugte einzelne Sessions koennen geloescht werden, zum Beispiel wegen Urla
|
|||||||
|
|
||||||
## Statistiken
|
## Statistiken
|
||||||
|
|
||||||
Die Seite `Statistiken` betrachtet immer einen Monat. Sie dient als Ueberblick fuer abgerechnete Zeiten, nicht abrechenbare Arbeit, Teamspace-Vergleich, Bewertungsstand und Auffaelligkeiten.
|
Die Seite `Statistiken` betrachtet immer einen Monat. Sie dient als Ueberblick fuer abgerechnete Zeiten, Teamspace-Vergleich, offene Bewertungen und Auffaelligkeiten.
|
||||||
|
|
||||||
### Kennzahlen
|
### Kennzahlen
|
||||||
|
|
||||||
@@ -349,15 +349,14 @@ Das bedeutet:
|
|||||||
Farben:
|
Farben:
|
||||||
|
|
||||||
- Abgerechnet: blau
|
- Abgerechnet: blau
|
||||||
- Teamspace: gruen
|
- Teamspace: violett
|
||||||
- Positive oder ausgeglichene Differenz: gruen
|
- Positive oder ausgeglichene Differenz: gruen
|
||||||
- Negative Differenz: rot
|
- Negative Differenz: rot
|
||||||
- Nicht abrechenbar: indigo
|
|
||||||
- Offen: gelb/orange
|
- Offen: gelb/orange
|
||||||
|
|
||||||
`Nicht abrechenbar` zeigt Sessions, die bewusst nicht fakturiert werden. Diese Minuten werden nicht mit Teamspace verglichen.
|
Nicht abrechenbare Sessions werden in den Statistiken nicht angezeigt.
|
||||||
|
|
||||||
`Offen` zeigt Sessions ohne Bewertung. Diese blockieren den Monatsabschluss.
|
`Offen` zeigt Sessions ohne Bewertung als reinen Pruefwert. Offene Zeiten werden nicht in Statistik- oder Vergleichswerte eingerechnet und blockieren den Monatsabschluss.
|
||||||
|
|
||||||
### Monatsverlauf und Abrechnungsqualitaet
|
### Monatsverlauf und Abrechnungsqualitaet
|
||||||
|
|
||||||
@@ -365,7 +364,7 @@ Diese Card kombiniert den Tagesverlauf und die Bewertungsqualitaet.
|
|||||||
|
|
||||||
- Balken zeigen abgerechnete Session-Zeit.
|
- Balken zeigen abgerechnete Session-Zeit.
|
||||||
- Die Linie zeigt Teamspace-Zeit.
|
- Die Linie zeigt Teamspace-Zeit.
|
||||||
- Der Bewertungsstand trennt abgerechnete, nicht abrechenbare und offene Sessions.
|
- Der Bewertungsstand trennt abgerechnete und offene Sessions.
|
||||||
- Der Teamspace-Abgleich zeigt die Gesamtdifferenz.
|
- Der Teamspace-Abgleich zeigt die Gesamtdifferenz.
|
||||||
- Manuell und Fix trennen manuell erfasste abrechenbare Sessions von automatisch erzeugten abrechenbaren Sessions.
|
- Manuell und Fix trennen manuell erfasste abrechenbare Sessions von automatisch erzeugten abrechenbaren Sessions.
|
||||||
|
|
||||||
@@ -377,7 +376,7 @@ Sichtbar sind:
|
|||||||
|
|
||||||
- Organisation
|
- Organisation
|
||||||
- Sessions
|
- Sessions
|
||||||
- Zeiten: Abgerechnet, nicht abrechenbar und Teamspace
|
- Zeiten: Abgerechnet und Teamspace
|
||||||
- Differenz
|
- Differenz
|
||||||
- Anteil an der abgerechneten Monatszeit
|
- Anteil an der abgerechneten Monatszeit
|
||||||
|
|
||||||
@@ -392,10 +391,9 @@ Sie zeigt je Typ:
|
|||||||
- Tickets
|
- Tickets
|
||||||
- Sessions
|
- Sessions
|
||||||
- Abgerechnete Session-Zeit
|
- Abgerechnete Session-Zeit
|
||||||
- Nicht abrechenbare Session-Zeit
|
|
||||||
- Teamspace-Zeit
|
- Teamspace-Zeit
|
||||||
- Differenz
|
- Differenz
|
||||||
- Anzahl abgerechnet, nicht abrechenbar und offen
|
- Anzahl abgerechnet und offen
|
||||||
|
|
||||||
### Ticket-Hotspots
|
### Ticket-Hotspots
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ export const helpSections: HelpSection[] = [
|
|||||||
{
|
{
|
||||||
id: "statistiken",
|
id: "statistiken",
|
||||||
title: "Statistiken",
|
title: "Statistiken",
|
||||||
description: "Statistiken vergleichen abgerechnete Session-Zeit, nicht abrechenbare Arbeit, offene Bewertungen und Teamspace-Werte.",
|
description: "Statistiken vergleichen abgerechnete Session-Zeit, offene Bewertungen und Teamspace-Werte.",
|
||||||
quickItems: [
|
quickItems: [
|
||||||
"Abgerechnet und Teamspace haben getrennte Farben.",
|
"Abgerechnet und Teamspace haben getrennte Farben.",
|
||||||
"Positive oder gleiche Differenz ist grün.",
|
"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.",
|
"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 positive Differenz bedeutet: In Teamspace steht mehr Zeit als abgerechnet wurde.",
|
||||||
"Eine negative Differenz bedeutet: In Teamspace steht weniger Zeit als abgerechnet wurde.",
|
"Eine negative Differenz bedeutet: In Teamspace steht weniger Zeit als abgerechnet wurde.",
|
||||||
"Nicht abrechenbar und Offen werden jeweils als eigene Werte ausgewiesen.",
|
"Nicht abrechenbar wird in den Statistiken nicht angezeigt.",
|
||||||
"Sie werden nicht in Relationen, Graphen, Teamspace-Differenzen, Hotspots, Manuell/Fix-Werte oder Durchschnittswerte eingerechnet.",
|
"Offen wird nur als Prüfwert angezeigt und 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.",
|
"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."
|
"Organisationen, Typen und Ticket-Hotspots helfen dabei, Auffälligkeiten im Monat schnell zu finden."
|
||||||
]
|
]
|
||||||
@@ -272,23 +272,6 @@ export const helpSections: HelpSection[] = [
|
|||||||
"Fehlende Teamspace-Werte können im Monatsabschluss geprüft und bei Bedarf zur Kenntnis genommen werden."
|
"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",
|
id: "statistik-offen",
|
||||||
title: "Statistik: Offen",
|
title: "Statistik: Offen",
|
||||||
@@ -318,8 +301,8 @@ export const helpSections: HelpSection[] = [
|
|||||||
details: [
|
details: [
|
||||||
"Der Monatsverlauf zeigt jeden Tag des Monats. Tage ohne Sessions bleiben leer, damit die Monatsstruktur sichtbar bleibt.",
|
"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 blauen Balken stehen für abgerechnete Session-Zeit.",
|
||||||
"Die grüne Linie steht für Teamspace-Zeit, sofern für den Tag Werte gepflegt wurden.",
|
"Die violette 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 Bewertungsstand weist Abgerechnet und Offen als getrennte Werte aus.",
|
||||||
"Der Teamspace-Abgleich zeigt die Gesamtdifferenz zwischen CRM-Werten und abgerechneter Session-Zeit.",
|
"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."
|
"Manuell und Fix trennen selbst erfasste abrechenbare Sessions von automatisch erzeugten abrechenbaren Sessions aus fixen Abrechnungen."
|
||||||
]
|
]
|
||||||
@@ -338,7 +321,6 @@ export const helpSections: HelpSection[] = [
|
|||||||
"Sessions zeigt die Anzahl der einzelnen Arbeitseinträge.",
|
"Sessions zeigt die Anzahl der einzelnen Arbeitseinträge.",
|
||||||
"Zeiten vergleicht abgerechnete Session-Zeit mit Teamspace-Zeit.",
|
"Zeiten vergleicht abgerechnete Session-Zeit mit Teamspace-Zeit.",
|
||||||
"Die Differenz wird als Teamspace minus abgerechnete Zeit berechnet.",
|
"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.",
|
"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."
|
"Bei vielen Organisationen wird die Liste paginiert, damit die Seite kompakt bleibt."
|
||||||
]
|
]
|
||||||
@@ -357,7 +339,7 @@ export const helpSections: HelpSection[] = [
|
|||||||
"Support und Consulting werden getrennt summiert.",
|
"Support und Consulting werden getrennt summiert.",
|
||||||
"Die Zeitwerte zeigen abgerechnete Sessions und Teamspace für den jeweiligen Typ.",
|
"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 Differenz folgt derselben Logik wie überall: Teamspace minus abgerechnete Session-Zeit.",
|
||||||
"Die Zeile Abr., Nicht und Offen zeigt, wie viele Sessions dieses Typs bereits bewertet oder noch offen sind."
|
"Die Zeile Abr. und Offen zeigt, wie viele Sessions dieses Typs bereits abgerechnet oder noch offen sind."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -372,7 +354,7 @@ export const helpSections: HelpSection[] = [
|
|||||||
details: [
|
details: [
|
||||||
"Ticket-Hotspots helfen, die größten Aufwandstreiber im Monat schnell zu erkennen.",
|
"Ticket-Hotspots helfen, die größten Aufwandstreiber im Monat schnell zu erkennen.",
|
||||||
"Die Liste zeigt die wichtigsten Tickets nach abgerechneter Zeit.",
|
"Die Liste zeigt die wichtigsten Tickets nach abgerechneter Zeit.",
|
||||||
"Pro Ticket siehst du Organisation, Sessionanzahl, aktive abrechenbare Tage, abgerechnete Zeit, nicht abrechenbare Zeit, Teamspace-Zeit und Differenz.",
|
"Pro Ticket siehst du Organisation, Sessionanzahl, aktive abrechenbare Tage, abgerechnete Zeit, Teamspace-Zeit und Differenz.",
|
||||||
"Offene Tickets werden mit einem Warnstatus angezeigt, damit Bewertungen nachgezogen werden können.",
|
"Offene Tickets werden mit einem Warnstatus angezeigt, damit Bewertungen nachgezogen werden können.",
|
||||||
"Über Öffnen gelangst du direkt in die Ticketdetailansicht des Monats."
|
"Über Öffnen gelangst du direkt in die Ticketdetailansicht des Monats."
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
CircleAlert,
|
CircleAlert,
|
||||||
Clock3,
|
Clock3,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
ListChecks,
|
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
Ticket,
|
Ticket,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
@@ -31,12 +30,11 @@ type StatisticsPageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const trackedTextClass = "text-sky-700 dark:text-sky-300";
|
const trackedTextClass = "text-sky-700 dark:text-sky-300";
|
||||||
const teamspaceTextClass = "text-emerald-700 dark:text-emerald-300";
|
const teamspaceTextClass = "text-violet-700 dark:text-violet-300";
|
||||||
const trackedFillClass = "fill-sky-500/70 dark:fill-sky-400/65";
|
const trackedFillClass = "fill-sky-500/70 dark:fill-sky-400/65";
|
||||||
const teamspaceStrokeClass = "text-emerald-600 dark:text-emerald-300";
|
const teamspaceStrokeClass = "text-violet-600 dark:text-violet-300";
|
||||||
const trackedDotClass = "bg-sky-500 dark:bg-sky-400";
|
const trackedDotClass = "bg-sky-500 dark:bg-sky-400";
|
||||||
const teamspaceDotClass = "bg-emerald-500 dark:bg-emerald-400";
|
const teamspaceDotClass = "bg-violet-500 dark:bg-violet-400";
|
||||||
const nonBillableTextClass = "text-indigo-700 dark:text-indigo-300";
|
|
||||||
const openTextClass = "text-amber-700 dark:text-amber-300";
|
const openTextClass = "text-amber-700 dark:text-amber-300";
|
||||||
const organizationPageSize = 8;
|
const organizationPageSize = 8;
|
||||||
|
|
||||||
@@ -125,10 +123,6 @@ function ColorLegend() {
|
|||||||
<span className="size-2 rounded-full bg-red-500 dark:bg-red-400" />
|
<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 className="text-red-700 dark:text-red-300">Differenz minus</span>
|
||||||
</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="inline-flex items-center gap-1.5">
|
||||||
<span className="size-2 rounded-full bg-amber-500 dark:bg-amber-400" />
|
<span className="size-2 rounded-full bg-amber-500 dark:bg-amber-400" />
|
||||||
<span className={openTextClass}>offen</span>
|
<span className={openTextClass}>offen</span>
|
||||||
@@ -195,10 +189,8 @@ function buildMonthSeries(month: string, buckets: StatisticsDailyBucket[]) {
|
|||||||
sessions: bucket?.sessions ?? 0,
|
sessions: bucket?.sessions ?? 0,
|
||||||
totalMinutes: bucket?.total_minutes ?? 0,
|
totalMinutes: bucket?.total_minutes ?? 0,
|
||||||
billedMinutes: bucket?.billed_minutes ?? 0,
|
billedMinutes: bucket?.billed_minutes ?? 0,
|
||||||
nonBillableMinutes: bucket?.non_billable_minutes ?? 0,
|
|
||||||
openMinutes: bucket?.open_minutes ?? 0,
|
openMinutes: bucket?.open_minutes ?? 0,
|
||||||
billedSessions: bucket?.billed_sessions ?? 0,
|
billedSessions: bucket?.billed_sessions ?? 0,
|
||||||
nonBillableSessions: bucket?.non_billable_sessions ?? 0,
|
|
||||||
openSessions: bucket?.open_sessions ?? 0,
|
openSessions: bucket?.open_sessions ?? 0,
|
||||||
crmBilledMinutes: bucket?.crm_billed_minutes ?? 0
|
crmBilledMinutes: bucket?.crm_billed_minutes ?? 0
|
||||||
};
|
};
|
||||||
@@ -345,7 +337,6 @@ function TicketHotspot({ ticket, month, onNavigate }: { ticket: StatisticsTicket
|
|||||||
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs">
|
<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={trackedTextClass}>Abgerechnet {formatMinutes(ticket.total_minutes)}</span>
|
||||||
<span className={teamspaceTextClass}>Teamspace {formatMinutes(ticket.crm_billed_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 className="text-muted-foreground">{ticket.billed_sessions} abgerechnet</span>
|
||||||
<span>{ticket.active_days} Tag(e)</span>
|
<span>{ticket.active_days} Tag(e)</span>
|
||||||
<span className={deltaTextClass(ticket.crm_delta_minutes)}>{formatTeamspaceDelta(ticket.crm_delta_minutes)}</span>
|
<span className={deltaTextClass(ticket.crm_delta_minutes)}>{formatTeamspaceDelta(ticket.crm_delta_minutes)}</span>
|
||||||
@@ -422,8 +413,6 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
const totals = stats?.totals;
|
const totals = stats?.totals;
|
||||||
const billedSessions = totals?.billedSessions ?? 0;
|
const billedSessions = totals?.billedSessions ?? 0;
|
||||||
const billedMinutes = totals?.billedMinutes ?? 0;
|
const billedMinutes = totals?.billedMinutes ?? 0;
|
||||||
const nonBillableSessions = totals?.nonBillableSessions ?? 0;
|
|
||||||
const nonBillableMinutes = totals?.nonBillableMinutes ?? 0;
|
|
||||||
const openSessions = totals?.openSessions ?? 0;
|
const openSessions = totals?.openSessions ?? 0;
|
||||||
const openMinutes = totals?.openMinutes ?? 0;
|
const openMinutes = totals?.openMinutes ?? 0;
|
||||||
const crmCoverage = percentage(totals?.crmBilledMinutes ?? 0, totals?.minutes ?? 0);
|
const crmCoverage = percentage(totals?.crmBilledMinutes ?? 0, totals?.minutes ?? 0);
|
||||||
@@ -471,7 +460,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Abgerechnet"
|
label="Abgerechnet"
|
||||||
value={formatMinutes(totals?.minutes ?? 0)}
|
value={formatMinutes(totals?.minutes ?? 0)}
|
||||||
@@ -490,20 +479,6 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
helpAnchor="statistik-teamspace"
|
helpAnchor="statistik-teamspace"
|
||||||
helpLabel="Hilfe zur Kennzahl 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
|
<MetricCard
|
||||||
label="Offen"
|
label="Offen"
|
||||||
value={formatMinutes(openMinutes)}
|
value={formatMinutes(openMinutes)}
|
||||||
@@ -544,9 +519,8 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
<span className="text-muted-foreground">Bewertungsstand</span>
|
<span className="text-muted-foreground">Bewertungsstand</span>
|
||||||
<span className="font-medium">{billedSessions} abrechenbar</span>
|
<span className="font-medium">{billedSessions} abrechenbar</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2 text-xs sm:grid-cols-3 xl:grid-cols-1 2xl:grid-cols-3">
|
<div className="grid gap-2 text-xs sm:grid-cols-2 xl:grid-cols-1 2xl:grid-cols-2">
|
||||||
<div className={trackedTextClass}>Abgerechnet: {billedSessions} Session(s), {formatMinutes(billedMinutes)}</div>
|
<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 className={openTextClass}>Offen: {openSessions} Session(s), {formatMinutes(openMinutes)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -590,7 +564,6 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
<p className="flex flex-wrap gap-x-3 gap-y-1 text-xs">
|
<p className="flex flex-wrap gap-x-3 gap-y-1 text-xs">
|
||||||
<span className={trackedTextClass}>Abgerechnet {formatMinutes(topOrganization.total_minutes)}</span>
|
<span className={trackedTextClass}>Abgerechnet {formatMinutes(topOrganization.total_minutes)}</span>
|
||||||
<span className={teamspaceTextClass}>Teamspace {formatMinutes(topOrganization.crm_billed_minutes)}</span>
|
<span className={teamspaceTextClass}>Teamspace {formatMinutes(topOrganization.crm_billed_minutes)}</span>
|
||||||
<span className={nonBillableTextClass}>Nicht {formatMinutes(topOrganization.non_billable_minutes)}</span>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -650,7 +623,6 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs">
|
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs">
|
||||||
<span className={trackedTextClass}>Abgerechnet {formatMinutes(organization.total_minutes)}</span>
|
<span className={trackedTextClass}>Abgerechnet {formatMinutes(organization.total_minutes)}</span>
|
||||||
<span className={teamspaceTextClass}>Teamspace {formatMinutes(organization.crm_billed_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="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>
|
<span className={deltaTextClass(organization.crm_delta_minutes)}>{formatTeamspaceDelta(organization.crm_delta_minutes)}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -721,9 +693,8 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
<span className={trackedTextClass}>Abgerechnet {formatMinutes(group.total_minutes)}</span>
|
<span className={trackedTextClass}>Abgerechnet {formatMinutes(group.total_minutes)}</span>
|
||||||
<span className={teamspaceTextClass}>Teamspace {formatMinutes(group.crm_billed_minutes)}</span>
|
<span className={teamspaceTextClass}>Teamspace {formatMinutes(group.crm_billed_minutes)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2 text-xs text-muted-foreground">
|
<div className="grid grid-cols-2 gap-2 text-xs text-muted-foreground">
|
||||||
<span>Abr. {group.billed_sessions} / {formatMinutes(group.billed_minutes)}</span>
|
<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>
|
<span>Offen {group.open_sessions} / {formatMinutes(group.open_minutes)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -761,7 +732,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
<Ticket className="size-8 text-muted-foreground" />
|
<Ticket className="size-8 text-muted-foreground" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">Keine abgerechneten Sessions in diesem Monat</p>
|
<p className="font-medium">Keine abgerechneten Sessions in diesem Monat</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>
|
<p className="text-sm text-muted-foreground">Nur abgerechnete Sessions fließen in Statistiken und Vergleiche ein.</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="secondary" size="sm" onClick={() => setMonth(currentMonth())}>
|
<Button variant="secondary" size="sm" onClick={() => setMonth(currentMonth())}>
|
||||||
<RotateCcw className="size-4" />
|
<RotateCcw className="size-4" />
|
||||||
|
|||||||
Reference in New Issue
Block a user