Exclude unresolved time from statistics
This commit is contained in:
@@ -779,8 +779,8 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) {
|
|||||||
COUNT(*) FILTER (WHERE billing_status IS NULL)::int AS open_sessions,
|
COUNT(*) FILTER (WHERE billing_status IS NULL)::int AS open_sessions,
|
||||||
COALESCE(SUM(rounded_minutes) FILTER (WHERE recurring_billing_id IS NOT NULL AND billing_status = 'billed'), 0)::int AS recurring_minutes,
|
COALESCE(SUM(rounded_minutes) FILTER (WHERE recurring_billing_id IS NOT NULL AND billing_status = 'billed'), 0)::int AS recurring_minutes,
|
||||||
COALESCE(SUM(rounded_minutes) FILTER (WHERE recurring_billing_id IS NULL AND billing_status = 'billed'), 0)::int AS manual_minutes,
|
COALESCE(SUM(rounded_minutes) FILTER (WHERE recurring_billing_id IS NULL AND billing_status = 'billed'), 0)::int AS manual_minutes,
|
||||||
COUNT(*) FILTER (WHERE recurring_billing_id IS NOT NULL)::int AS recurring_sessions,
|
COUNT(*) FILTER (WHERE recurring_billing_id IS NOT NULL AND billing_status = 'billed')::int AS recurring_sessions,
|
||||||
COUNT(*) FILTER (WHERE recurring_billing_id IS NULL)::int AS manual_sessions,
|
COUNT(*) FILTER (WHERE recurring_billing_id IS NULL AND billing_status = 'billed')::int AS manual_sessions,
|
||||||
COALESCE(ROUND(AVG(rounded_minutes) FILTER (WHERE billing_status = 'billed')), 0)::int AS average_session_minutes,
|
COALESCE(ROUND(AVG(rounded_minutes) FILTER (WHERE billing_status = 'billed')), 0)::int AS average_session_minutes,
|
||||||
COUNT(DISTINCT day) FILTER (WHERE billing_status = 'billed')::int AS active_days,
|
COUNT(DISTINCT day) FILTER (WHERE billing_status = 'billed')::int AS active_days,
|
||||||
COALESCE((SELECT SUM(crm_billed_minutes)::int FROM ticket_day_billing), 0)::int AS crm_billed_minutes
|
COALESCE((SELECT SUM(crm_billed_minutes)::int FROM ticket_day_billing), 0)::int AS crm_billed_minutes
|
||||||
@@ -889,6 +889,7 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) {
|
|||||||
COALESCE((SELECT SUM(crm_billed_minutes)::int FROM ticket_day_billing tdb WHERE tdb.ticket_id = session_base.ticket_id), 0)::int AS crm_billed_minutes
|
COALESCE((SELECT SUM(crm_billed_minutes)::int FROM ticket_day_billing tdb WHERE tdb.ticket_id = session_base.ticket_id), 0)::int AS crm_billed_minutes
|
||||||
FROM session_base
|
FROM session_base
|
||||||
GROUP BY ticket_id, ticket_number
|
GROUP BY ticket_id, ticket_number
|
||||||
|
HAVING SUM(rounded_minutes) FILTER (WHERE billing_status = 'billed') > 0
|
||||||
ORDER BY total_minutes DESC, ticket_number ASC
|
ORDER BY total_minutes DESC, ticket_number ASC
|
||||||
LIMIT 12;
|
LIMIT 12;
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -332,6 +332,8 @@ Die Seite `Statistiken` betrachtet immer einen Monat. Sie dient als Ueberblick f
|
|||||||
|
|
||||||
`Abgerechnet` zeigt die Summe aller Sessions mit Bewertung `Abgerechnet`. Nicht abrechenbare und offene Sessions sind darin nicht enthalten. Dieser Wert ist die Basis fuer den Vergleich mit Teamspace.
|
`Abgerechnet` zeigt die Summe aller Sessions mit Bewertung `Abgerechnet`. Nicht abrechenbare und offene Sessions sind darin nicht enthalten. Dieser Wert ist die Basis fuer den Vergleich mit Teamspace.
|
||||||
|
|
||||||
|
Alle Statistik- und Vergleichswerte verwenden nur abgerechnete Zeiten. Nicht ausgewaehlte/offene und nicht abrechenbare Sessions werden nicht in Relationen, Graphen, Teamspace-Differenzen, Hotspots, Manuell/Fix-Werte oder Durchschnittswerte eingerechnet.
|
||||||
|
|
||||||
`Teamspace` zeigt die Summe aller Teamspace-Tageswerte im Monat. Die Differenz wird immer so berechnet:
|
`Teamspace` zeigt die Summe aller Teamspace-Tageswerte im Monat. Die Differenz wird immer so berechnet:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -227,10 +227,12 @@ export const helpSections: HelpSection[] = [
|
|||||||
details: [
|
details: [
|
||||||
"Die Statistikseite betrachtet immer einen Monat.",
|
"Die Statistikseite betrachtet immer einen Monat.",
|
||||||
"Abgerechnet zeigt nur Sessions mit der Bewertung Abgerechnet. Nicht abrechenbare und offene Zeiten sind dort nicht enthalten.",
|
"Abgerechnet zeigt nur Sessions mit der Bewertung Abgerechnet. Nicht abrechenbare und offene Zeiten sind dort nicht enthalten.",
|
||||||
|
"Alle Statistik- und Vergleichswerte nutzen nur abgerechnete Zeiten.",
|
||||||
"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 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.",
|
"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."
|
||||||
]
|
]
|
||||||
@@ -248,7 +250,7 @@ export const helpSections: HelpSection[] = [
|
|||||||
"Abgerechnet umfasst nur Sessions des angemeldeten Benutzers, die aktiv als Abgerechnet markiert wurden.",
|
"Abgerechnet umfasst nur Sessions des angemeldeten Benutzers, die aktiv als Abgerechnet markiert wurden.",
|
||||||
"Nicht abrechenbare Sessions und offene Sessions werden aus diesem Wert herausgehalten.",
|
"Nicht abrechenbare Sessions und offene Sessions werden aus diesem Wert herausgehalten.",
|
||||||
"Auch automatisch erzeugte Sessions aus fixen Abrechnungen zählen hier mit, sobald sie als Abgerechnet markiert wurden.",
|
"Auch automatisch erzeugte Sessions aus fixen Abrechnungen zählen hier mit, sobald sie als Abgerechnet markiert wurden.",
|
||||||
"Die Prozentwerte beziehen sich auf alle Sessions des Monats.",
|
"Es werden keine offenen oder nicht abrechenbaren Zeiten in diesen Wert eingerechnet.",
|
||||||
"Dieser Wert ist die Ausgangsbasis für Teamspace-Differenzen: Teamspace minus abgerechnete Session-Zeit ergibt die Differenz."
|
"Dieser Wert ist die Ausgangsbasis für Teamspace-Differenzen: Teamspace minus abgerechnete Session-Zeit ergibt die Differenz."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -276,14 +278,14 @@ export const helpSections: HelpSection[] = [
|
|||||||
description: "Diese Kennzahl zeigt Arbeit, die bewusst nicht abgerechnet wird.",
|
description: "Diese Kennzahl zeigt Arbeit, die bewusst nicht abgerechnet wird.",
|
||||||
quickItems: [
|
quickItems: [
|
||||||
"Nur Sessions mit Bewertung Nicht abrechenbar.",
|
"Nur Sessions mit Bewertung Nicht abrechenbar.",
|
||||||
"Der Prozentwert bezieht sich auf die Sessionanzahl.",
|
"Wird nur separat ausgewiesen.",
|
||||||
"Nicht abrechenbare Sessions blockieren den Monatsabschluss nicht."
|
"Nicht abrechenbare Sessions blockieren den Monatsabschluss nicht."
|
||||||
],
|
],
|
||||||
details: [
|
details: [
|
||||||
"Nicht abrechenbar ist getrennt von Abgerechnet, damit interne, Kulanz- oder nicht fakturierbare Arbeit sichtbar bleibt.",
|
"Nicht abrechenbar ist getrennt von Abgerechnet, damit interne, Kulanz- oder nicht fakturierbare Arbeit sichtbar bleibt.",
|
||||||
"Diese Minuten werden nicht mit Teamspace verglichen.",
|
"Diese Minuten werden nicht mit Teamspace verglichen.",
|
||||||
"Die Zeit zeigt die Summe der nicht abrechenbaren Session-Minuten.",
|
"Die Zeit zeigt die Summe der nicht abrechenbaren Session-Minuten.",
|
||||||
"Die Prozentwerte werden zusammen mit abgerechneten und offenen Sessions gerundet, sodass alle Zustände zusammen 100 Prozent ergeben.",
|
"Es werden daraus keine Anteile, Differenzen oder Vergleichswerte berechnet.",
|
||||||
"Wenn eine Bewertung zurückgenommen wird, wandert die Session wieder in Offen."
|
"Wenn eine Bewertung zurückgenommen wird, wandert die Session wieder in Offen."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -307,7 +309,7 @@ export const helpSections: HelpSection[] = [
|
|||||||
{
|
{
|
||||||
id: "statistik-verlauf",
|
id: "statistik-verlauf",
|
||||||
title: "Statistik: Monatsverlauf und Qualität",
|
title: "Statistik: Monatsverlauf und Qualität",
|
||||||
description: "Diese Card verbindet Tagesverlauf, Teamspace-Linie, Bewertungsstand und Qualitätswerte.",
|
description: "Diese Card verbindet Tagesverlauf, Teamspace-Linie und reine Statuswerte.",
|
||||||
quickItems: [
|
quickItems: [
|
||||||
"Balken zeigen abgerechnete Sessions.",
|
"Balken zeigen abgerechnete Sessions.",
|
||||||
"Die Linie zeigt Teamspace-Werte.",
|
"Die Linie zeigt Teamspace-Werte.",
|
||||||
@@ -317,7 +319,7 @@ export const helpSections: HelpSection[] = [
|
|||||||
"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 grüne Linie steht für Teamspace-Zeit, sofern für den Tag Werte gepflegt wurden.",
|
||||||
"Der Bewertungsstand trennt Abgerechnet, Nicht abrechenbar und Offen.",
|
"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.",
|
"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."
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -60,32 +60,6 @@ function percentage(value: number, total: number) {
|
|||||||
return Math.max(0, Math.min(100, Math.round((value / total) * 100)));
|
return Math.max(0, Math.min(100, Math.round((value / total) * 100)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function roundedPercentages(values: number[]) {
|
|
||||||
const total = values.reduce((sum, value) => sum + value, 0);
|
|
||||||
|
|
||||||
if (total <= 0) {
|
|
||||||
return values.map(() => 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = values.map((value) => (value / total) * 100);
|
|
||||||
const floors = raw.map(Math.floor);
|
|
||||||
let remainder = 100 - floors.reduce((sum, value) => sum + value, 0);
|
|
||||||
const order = raw
|
|
||||||
.map((value, index) => ({ index, fraction: value - Math.floor(value) }))
|
|
||||||
.sort((left, right) => right.fraction - left.fraction);
|
|
||||||
|
|
||||||
for (const item of order) {
|
|
||||||
if (remainder <= 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
floors[item.index] += 1;
|
|
||||||
remainder -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return floors;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSignedMinutes(minutes: number) {
|
function formatSignedMinutes(minutes: number) {
|
||||||
if (minutes === 0) {
|
if (minutes === 0) {
|
||||||
return "ausgeglichen";
|
return "ausgeglichen";
|
||||||
@@ -341,10 +315,10 @@ function OrganizationRow({ organization, maxMinutes, totalMinutes }: { organizat
|
|||||||
<TableCell className="max-w-[320px] whitespace-normal">
|
<TableCell className="max-w-[320px] whitespace-normal">
|
||||||
<div className="font-medium">{organization.organization_name ?? "Keine Organisation"}</div>
|
<div className="font-medium">{organization.organization_name ?? "Keine Organisation"}</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{organization.tickets} Ticket(s) · {organization.sessions} Session(s)
|
{organization.tickets} Ticket(s) · {organization.billed_sessions} abgerechnet
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{organization.sessions}</TableCell>
|
<TableCell>{organization.billed_sessions}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<TimeComparison trackedMinutes={organization.total_minutes} crmMinutes={organization.crm_billed_minutes} />
|
<TimeComparison trackedMinutes={organization.total_minutes} crmMinutes={organization.crm_billed_minutes} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -372,7 +346,7 @@ function TicketHotspot({ ticket, month, onNavigate }: { ticket: StatisticsTicket
|
|||||||
<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={nonBillableTextClass}>Nicht {formatMinutes(ticket.non_billable_minutes)}</span>
|
||||||
<span className="text-muted-foreground">{ticket.sessions} Session(s)</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>
|
||||||
</div>
|
</div>
|
||||||
@@ -452,8 +426,6 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
const nonBillableMinutes = totals?.nonBillableMinutes ?? 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 [billedSessionShare, nonBillableSessionShare, openSessionShare] = roundedPercentages([billedSessions, nonBillableSessions, openSessions]);
|
|
||||||
const [billedTimeShare, nonBillableTimeShare, openTimeShare] = roundedPercentages([billedMinutes, nonBillableMinutes, openMinutes]);
|
|
||||||
const crmCoverage = percentage(totals?.crmBilledMinutes ?? 0, totals?.minutes ?? 0);
|
const crmCoverage = percentage(totals?.crmBilledMinutes ?? 0, totals?.minutes ?? 0);
|
||||||
const maxOrganizationMinutes = Math.max(...(stats?.organizations ?? []).map((organization) => organization.total_minutes), 1);
|
const maxOrganizationMinutes = Math.max(...(stats?.organizations ?? []).map((organization) => organization.total_minutes), 1);
|
||||||
const topOrganization = stats?.organizations[0] ?? null;
|
const topOrganization = stats?.organizations[0] ?? null;
|
||||||
@@ -503,7 +475,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
<MetricCard
|
<MetricCard
|
||||||
label="Abgerechnet"
|
label="Abgerechnet"
|
||||||
value={formatMinutes(totals?.minutes ?? 0)}
|
value={formatMinutes(totals?.minutes ?? 0)}
|
||||||
detail={`${billedSessions} von ${totals?.sessions ?? 0} Session(s), ${billedSessionShare}%`}
|
detail={`${billedSessions} Session(s), ${totals?.tickets ?? 0} Ticket(s)`}
|
||||||
icon={Clock3}
|
icon={Clock3}
|
||||||
valueClassName={trackedTextClass}
|
valueClassName={trackedTextClass}
|
||||||
helpAnchor="statistik-getrackt"
|
helpAnchor="statistik-getrackt"
|
||||||
@@ -523,8 +495,8 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
value={formatMinutes(nonBillableMinutes)}
|
value={formatMinutes(nonBillableMinutes)}
|
||||||
detail={
|
detail={
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div>{nonBillableSessions} von {totals?.sessions ?? 0} Session(s), {nonBillableSessionShare}%</div>
|
<div>{nonBillableSessions} Session(s)</div>
|
||||||
<div>{nonBillableTimeShare}% der erfassten Zeit</div>
|
<div>wird nicht kalkuliert</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
icon={ListChecks}
|
icon={ListChecks}
|
||||||
@@ -537,8 +509,8 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
value={formatMinutes(openMinutes)}
|
value={formatMinutes(openMinutes)}
|
||||||
detail={
|
detail={
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div>{openSessions} von {totals?.sessions ?? 0} Session(s), {openSessionShare}%</div>
|
<div>{openSessions} Session(s)</div>
|
||||||
<div>{openTimeShare}% der Zeit</div>
|
<div>wird nicht kalkuliert</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
icon={CircleAlert}
|
icon={CircleAlert}
|
||||||
@@ -570,12 +542,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
<div className="space-y-2 rounded-md border bg-muted/20 p-3">
|
<div className="space-y-2 rounded-md border bg-muted/20 p-3">
|
||||||
<div className="flex items-center justify-between gap-3 text-sm">
|
<div className="flex items-center justify-between gap-3 text-sm">
|
||||||
<span className="text-muted-foreground">Bewertungsstand</span>
|
<span className="text-muted-foreground">Bewertungsstand</span>
|
||||||
<span className="font-medium">{billedSessionShare}% / {nonBillableSessionShare}% / {openSessionShare}%</span>
|
<span className="font-medium">{billedSessions} abrechenbar</span>
|
||||||
</div>
|
|
||||||
<div className="flex h-2 overflow-hidden rounded-full bg-muted">
|
|
||||||
<div className="bg-sky-500 dark:bg-sky-400" style={{ width: `${billedSessionShare}%` }} />
|
|
||||||
<div className="bg-rose-500 dark:bg-rose-400" style={{ width: `${nonBillableSessionShare}%` }} />
|
|
||||||
<div className="bg-amber-500 dark:bg-amber-400" style={{ width: `${openSessionShare}%` }} />
|
|
||||||
</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-3 xl:grid-cols-1 2xl:grid-cols-3">
|
||||||
<div className={trackedTextClass}>Abgerechnet: {billedSessions} Session(s), {formatMinutes(billedMinutes)}</div>
|
<div className={trackedTextClass}>Abgerechnet: {billedSessions} Session(s), {formatMinutes(billedMinutes)}</div>
|
||||||
@@ -592,7 +559,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
<DeltaBadge minutes={totals?.crmDeltaMinutes ?? 0} />
|
<DeltaBadge minutes={totals?.crmDeltaMinutes ?? 0} />
|
||||||
</div>
|
</div>
|
||||||
<TimeComparison trackedMinutes={totals?.minutes ?? 0} crmMinutes={totals?.crmBilledMinutes ?? 0} sessions={totals?.sessions ?? 0} />
|
<TimeComparison trackedMinutes={totals?.minutes ?? 0} crmMinutes={totals?.crmBilledMinutes ?? 0} sessions={totals?.billedSessions ?? 0} />
|
||||||
<div className="text-xs text-muted-foreground">Teamspace-Abdeckung: {crmCoverage}% der abgerechneten Zeit</div>
|
<div className="text-xs text-muted-foreground">Teamspace-Abdeckung: {crmCoverage}% der abgerechneten Zeit</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -651,7 +618,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Organisation</TableHead>
|
<TableHead>Organisation</TableHead>
|
||||||
<TableHead>Sessions</TableHead>
|
<TableHead>Abgerechnet</TableHead>
|
||||||
<TableHead>Zeiten</TableHead>
|
<TableHead>Zeiten</TableHead>
|
||||||
<TableHead>Differenz</TableHead>
|
<TableHead>Differenz</TableHead>
|
||||||
<TableHead>Anteil abgerechnet</TableHead>
|
<TableHead>Anteil abgerechnet</TableHead>
|
||||||
@@ -675,7 +642,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate font-medium">{organization.organization_name ?? "Keine Organisation"}</p>
|
<p className="truncate font-medium">{organization.organization_name ?? "Keine Organisation"}</p>
|
||||||
<p className="text-xs text-muted-foreground">{organization.tickets} Ticket(s) · {organization.sessions} Session(s)</p>
|
<p className="text-xs text-muted-foreground">{organization.tickets} Ticket(s) · {organization.billed_sessions} abgerechnet</p>
|
||||||
</div>
|
</div>
|
||||||
<DeltaBadge minutes={organization.crm_delta_minutes} />
|
<DeltaBadge minutes={organization.crm_delta_minutes} />
|
||||||
</div>
|
</div>
|
||||||
@@ -745,7 +712,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold">{workTypeLabel(group.work_type)}</p>
|
<p className="font-semibold">{workTypeLabel(group.work_type)}</p>
|
||||||
<p className="text-xs text-muted-foreground">{group.tickets} Ticket(s), {group.sessions} Session(s)</p>
|
<p className="text-xs text-muted-foreground">{group.tickets} Ticket(s), {group.billed_sessions} abgerechnet</p>
|
||||||
</div>
|
</div>
|
||||||
<DeltaBadge minutes={group.crm_delta_minutes} />
|
<DeltaBadge minutes={group.crm_delta_minutes} />
|
||||||
</div>
|
</div>
|
||||||
@@ -788,13 +755,13 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{stats && stats.totals.sessions === 0 ? (
|
{stats && stats.totals.billedSessions === 0 ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="grid place-items-center gap-2 p-8 text-center">
|
<CardContent className="grid place-items-center gap-2 p-8 text-center">
|
||||||
<Ticket className="size-8 text-muted-foreground" />
|
<Ticket className="size-8 text-muted-foreground" />
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">Keine Sessions in diesem Monat</p>
|
<p className="font-medium">Keine abgerechneten Sessions in diesem Monat</p>
|
||||||
<p className="text-sm text-muted-foreground">Sobald Sessions vorhanden sind, füllt sich die Statistik automatisch.</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>
|
</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