Use billable time for Teamspace comparisons

This commit is contained in:
2026-08-05 17:06:34 +02:00
parent 76bc438b4e
commit 2aec561f3c
8 changed files with 169 additions and 130 deletions
+12 -11
View File
@@ -384,8 +384,9 @@ export function AnalysisPage({ onNavigate }: AnalysisPageProps) {
const metricCards: Array<{ label: string; value: string | number; detail?: string; icon: LucideIcon }> = [
{ label: "Tickets", value: totals?.tickets ?? 0, icon: Ticket },
{ label: "Sessions", value: totals?.sessions ?? 0, icon: Users },
{ label: "Zeit", value: formatMinutes(totals?.minutes ?? 0), detail: `Teamspace ${formatMinutes(totals?.crmBilledMinutes ?? 0)}`, icon: Clock3 },
{ label: "Offen", value: totals?.openSessions ?? 0, icon: UserCheck }
{ label: "Abgerechnet", value: formatMinutes(totals?.minutes ?? 0), detail: `Teamspace ${formatMinutes(totals?.crmBilledMinutes ?? 0)}`, icon: Clock3 },
{ label: "Nicht abrechenbar", value: formatMinutes(totals?.nonBillableMinutes ?? 0), icon: UserCheck },
{ label: "Offen", value: totals?.openSessions ?? 0, detail: formatMinutes(totals?.openMinutes ?? 0), icon: UserCheck }
];
const tickets = overview?.tickets ?? [];
const organizationOptions = useMemo(() => {
@@ -495,7 +496,7 @@ export function AnalysisPage({ onNavigate }: AnalysisPageProps) {
</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-5">
{metricCards.map(({ label, value, detail, icon: Icon }) => (
<Card key={label}>
<CardContent className="flex items-center gap-3 p-3">
@@ -515,9 +516,9 @@ export function AnalysisPage({ onNavigate }: AnalysisPageProps) {
<Card>
<CardHeader className="gap-3 p-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<div>
<CardTitle>Gesamtaufwand</CardTitle>
<CardTitle>Abgerechneter Aufwand</CardTitle>
<CardDescription>
{periodType === "month" ? "Aufwand pro Tag im ausgewählten Monat." : "Aufwand pro Stunde am ausgewählten Tag."}
{periodType === "month" ? "Abrechenbare Zeit pro Tag im ausgewählten Monat." : "Abrechenbare Zeit pro Stunde am ausgewählten Tag."}
</CardDescription>
</div>
<div className="flex items-center gap-2">
@@ -591,7 +592,7 @@ export function AnalysisPage({ onNavigate }: AnalysisPageProps) {
<div className="mt-3 flex flex-wrap gap-3 text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<span className="size-2 rounded-sm bg-neutral-300 dark:bg-neutral-700" />
Minuten
Abgerechnet
</span>
<span className="inline-flex items-center gap-1.5">
<span className="h-0.5 w-4 bg-foreground" />
@@ -749,8 +750,8 @@ export function AnalysisPage({ onNavigate }: AnalysisPageProps) {
<TableHead>Organisation</TableHead>
<TableHead>Art</TableHead>
<TableHead>Sessions</TableHead>
<TableHead>Zeit</TableHead>
<TableHead>Abgerechnet</TableHead>
<TableHead>Zeit abgerechnet</TableHead>
<TableHead>Sessions abgerechnet</TableHead>
<TableHead>Nicht abrechenbar</TableHead>
<TableHead>Status</TableHead>
<TableHead />
@@ -789,7 +790,7 @@ export function AnalysisPage({ onNavigate }: AnalysisPageProps) {
<TableCell>{ticket.session_count}</TableCell>
<TableCell>{formatMinutes(ticket.total_minutes)}</TableCell>
<TableCell>{ticket.billed_count}</TableCell>
<TableCell>{ticket.non_billable_count}</TableCell>
<TableCell>{ticket.non_billable_count} / {formatMinutes(ticket.non_billable_minutes)}</TableCell>
<TableCell>
<TicketStatus ticket={ticket} />
</TableCell>
@@ -837,7 +838,7 @@ export function AnalysisPage({ onNavigate }: AnalysisPageProps) {
</div>
<div className="grid grid-cols-3 gap-2 text-sm">
<div className="rounded-md bg-muted/50 p-2">
<p className="text-muted-foreground">Zeit</p>
<p className="text-muted-foreground">Abr.</p>
<p className="font-medium">{formatMinutes(ticket.total_minutes)}</p>
</div>
<div className="rounded-md bg-muted/50 p-2">
@@ -846,7 +847,7 @@ export function AnalysisPage({ onNavigate }: AnalysisPageProps) {
</div>
<div className="rounded-md bg-muted/50 p-2">
<p className="text-muted-foreground">Nicht</p>
<p className="font-medium">{ticket.non_billable_count}</p>
<p className="font-medium">{formatMinutes(ticket.non_billable_minutes)}</p>
</div>
</div>
<Button className="w-full" size="sm" variant="secondary" onClick={() => onNavigate(`/analysis/${periodType}/${period}/tickets/${ticket.id}`)}>
+3 -3
View File
@@ -136,7 +136,7 @@ function CrmDayItem({
</div>
<p className="truncate text-sm text-muted-foreground">{item.organization_name}</p>
<p className="flex flex-wrap gap-x-3 gap-y-1 text-xs">
<span className={trackedTextClass}>Sessions {formatMinutes(item.tracked_minutes)}</span>
<span className={trackedTextClass}>Abgerechnet {formatMinutes(item.tracked_minutes)}</span>
<span className={teamspaceTextClass}>Teamspace {formatMinutes(item.crm_billed_minutes)}</span>
{typeof item.delta_minutes === "number" ? <span className="text-muted-foreground">Differenz {formatTeamspaceDelta(item.delta_minutes)}</span> : null}
</p>
@@ -331,7 +331,7 @@ export function MonthlyClosePage({ onNavigate }: MonthlyClosePageProps) {
) : null}
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-5">
<StatusCard label="Sessions" value={stats?.totals.sessions ?? 0} detail={`${formatMinutes(stats?.totals.minutes ?? 0)} getrackt`} />
<StatusCard label="Sessions" value={stats?.totals.sessions ?? 0} detail={`${formatMinutes(stats?.totals.minutes ?? 0)} abgerechnet`} />
<StatusCard label="Teamspace" value={formatMinutes(stats?.totals.crmBilledMinutes ?? 0)} detail={`Differenz ${formatTeamspaceDelta(stats?.totals.crmDeltaMinutes ?? 0)}`} />
<StatusCard label="Offene Bewertungen" value={openSessions.length} detail={`${openTicketGroups.length} Ticket(s)`} tone={openSessions.length > 0 ? "warn" : "ok"} />
<StatusCard label="Ohne Teamspace" value={missingCrmDays.length} detail={`${acknowledgedMissingCrmDays.length} zur Kenntnis genommen`} tone={missingCrmDays.length > 0 ? "warn" : "ok"} />
@@ -409,7 +409,7 @@ export function MonthlyClosePage({ onNavigate }: MonthlyClosePageProps) {
))}
{visibleMissingCrmDays.length === 0 ? (
<p className="rounded-md border bg-muted/20 p-3 text-sm text-muted-foreground">
{acknowledgedMissingCrmDays.length > 0 ? "Alle offenen Punkte sind zur Kenntnis genommen." : "Alle getrackten Tage haben einen Teamspace-Wert."}
{acknowledgedMissingCrmDays.length > 0 ? "Alle offenen Punkte sind zur Kenntnis genommen." : "Alle abrechenbaren Tage haben einen Teamspace-Wert."}
</p>
) : null}
</section>
+46 -37
View File
@@ -36,7 +36,7 @@ const trackedFillClass = "fill-sky-500/70 dark:fill-sky-400/65";
const teamspaceStrokeClass = "text-emerald-600 dark:text-emerald-300";
const trackedDotClass = "bg-sky-500 dark:bg-sky-400";
const teamspaceDotClass = "bg-emerald-500 dark:bg-emerald-400";
const evaluatedTextClass = "text-violet-700 dark:text-violet-300";
const nonBillableTextClass = "text-rose-700 dark:text-rose-300";
const openTextClass = "text-amber-700 dark:text-amber-300";
const organizationPageSize = 8;
@@ -112,20 +112,20 @@ function DeltaBadge({ minutes }: { minutes: number }) {
function deltaDescription(minutes: number) {
if (minutes > 0) {
return "Teamspace liegt über den Sessions";
return "Teamspace liegt über der abgerechneten Zeit";
}
if (minutes < 0) {
return "Teamspace liegt unter den Sessions";
return "Teamspace liegt unter der abgerechneten Zeit";
}
return "Teamspace und Sessions sind ausgeglichen";
return "Teamspace und abgerechnete Zeit sind ausgeglichen";
}
function TimeComparison({ trackedMinutes, crmMinutes, sessions }: { trackedMinutes: number; crmMinutes: number; sessions?: number }) {
return (
<div className="space-y-0.5 text-sm">
<div className={`font-semibold ${trackedTextClass}`}>Sessions {formatMinutes(trackedMinutes)}</div>
<div className={`font-semibold ${trackedTextClass}`}>Abgerechnet {formatMinutes(trackedMinutes)}</div>
<div className={`font-semibold ${teamspaceTextClass}`}>Teamspace {formatMinutes(crmMinutes)}</div>
{typeof sessions === "number" ? <div className="text-xs text-muted-foreground">{sessions} Session(s)</div> : null}
</div>
@@ -137,7 +137,7 @@ function ColorLegend() {
<div className="flex flex-wrap gap-x-4 gap-y-2 rounded-md border bg-muted/20 p-3 text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<span className={`size-2 rounded-sm ${trackedDotClass}`} />
<span className={trackedTextClass}>Sessions</span>
<span className={trackedTextClass}>Abgerechnet</span>
</span>
<span className="inline-flex items-center gap-1.5">
<span className={`h-0.5 w-4 ${teamspaceDotClass}`} />
@@ -152,8 +152,8 @@ function ColorLegend() {
<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-violet-500 dark:bg-violet-400" />
<span className={evaluatedTextClass}>bewertet</span>
<span className="size-2 rounded-full bg-rose-500 dark:bg-rose-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" />
@@ -369,8 +369,9 @@ function TicketHotspot({ ticket, month, onNavigate }: { ticket: StatisticsTicket
</div>
<p className="truncate text-sm text-muted-foreground">{ticket.organization_name ?? "Keine Organisation"}</p>
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs">
<span className={trackedTextClass}>Sessions {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={nonBillableTextClass}>Nicht {formatMinutes(ticket.non_billable_minutes)}</span>
<span className="text-muted-foreground">{ticket.sessions} Session(s)</span>
<span>{ticket.active_days} Tag(e)</span>
<span className={deltaTextClass(ticket.crm_delta_minutes)}>{formatTeamspaceDelta(ticket.crm_delta_minutes)}</span>
@@ -445,10 +446,14 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
}
const totals = stats?.totals;
const evaluatedSessions = (totals?.billedSessions ?? 0) + (totals?.nonBillableSessions ?? 0);
const evaluatedMinutes = (totals?.billedMinutes ?? 0) + (totals?.nonBillableMinutes ?? 0);
const [evaluatedSessionShare, openSessionShare] = roundedPercentages([evaluatedSessions, totals?.openSessions ?? 0]);
const [evaluatedTimeShare, openTimeShare] = roundedPercentages([evaluatedMinutes, totals?.openMinutes ?? 0]);
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 [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 maxOrganizationMinutes = Math.max(...(stats?.organizations ?? []).map((organization) => organization.total_minutes), 1);
const topOrganization = stats?.organizations[0] ?? null;
@@ -496,13 +501,13 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<MetricCard
label="Getrackt"
label="Abgerechnet"
value={formatMinutes(totals?.minutes ?? 0)}
detail={`${totals?.sessions ?? 0} Session(s), ${totals?.tickets ?? 0} Ticket(s)`}
detail={`${billedSessions} von ${totals?.sessions ?? 0} Session(s), ${billedSessionShare}%`}
icon={Clock3}
valueClassName={trackedTextClass}
helpAnchor="statistik-getrackt"
helpLabel="Hilfe zur Kennzahl Getrackt"
helpLabel="Hilfe zur Kennzahl Abgerechnet"
/>
<MetricCard
label="Teamspace"
@@ -514,25 +519,25 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
helpLabel="Hilfe zur Kennzahl Teamspace"
/>
<MetricCard
label="Bewertet"
value={formatMinutes(evaluatedMinutes)}
label="Nicht abrechenbar"
value={formatMinutes(nonBillableMinutes)}
detail={
<div className="space-y-0.5">
<div>{evaluatedSessions} von {totals?.sessions ?? 0} Session(s), {evaluatedSessionShare}%</div>
<div>{evaluatedTimeShare}% der Zeit · Abr. {totals?.billedSessions ?? 0} · Nicht {totals?.nonBillableSessions ?? 0}</div>
<div>{nonBillableSessions} von {totals?.sessions ?? 0} Session(s), {nonBillableSessionShare}%</div>
<div>{nonBillableTimeShare}% der erfassten Zeit</div>
</div>
}
icon={ListChecks}
valueClassName={evaluatedTextClass}
valueClassName={nonBillableTextClass}
helpAnchor="statistik-bewertet"
helpLabel="Hilfe zur Kennzahl Bewertet"
helpLabel="Hilfe zur Kennzahl Nicht abrechenbar"
/>
<MetricCard
label="Offen"
value={formatMinutes(totals?.openMinutes ?? 0)}
value={formatMinutes(openMinutes)}
detail={
<div className="space-y-0.5">
<div>{totals?.openSessions ?? 0} von {totals?.sessions ?? 0} Session(s), {openSessionShare}%</div>
<div>{openSessions} von {totals?.sessions ?? 0} Session(s), {openSessionShare}%</div>
<div>{openTimeShare}% der Zeit</div>
</div>
}
@@ -547,7 +552,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<CardHeader className="gap-3 p-4 sm:flex-row sm:items-start sm:justify-between sm:space-y-0">
<div>
<CardTitle>Monatsverlauf und Abrechnungsqualität</CardTitle>
<CardDescription>Getrackter Aufwand pro Tag, Teamspace-Linie und Bewertungsstand in einem Überblick.</CardDescription>
<CardDescription>Abgerechnete Zeit pro Tag, Teamspace-Linie und Bewertungsstand in einem Überblick.</CardDescription>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline">{crmCoverage}% Teamspace-Abdeckung</Badge>
@@ -558,22 +563,24 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<div className="space-y-3">
<DailyEffortChart month={month} buckets={stats?.dailySeries ?? []} />
<ColorLegend />
{strongestDay ? <p className="text-xs text-muted-foreground">Stärkster Tag: {formatDate(`${strongestDay.day}T00:00:00`)} mit {formatMinutes(strongestDay.total_minutes)}</p> : null}
{strongestDay ? <p className="text-xs text-muted-foreground">Stärkster abrechenbarer Tag: {formatDate(`${strongestDay.day}T00:00:00`)} mit {formatMinutes(strongestDay.total_minutes)}</p> : null}
</div>
<div className="space-y-4">
<div className="space-y-2 rounded-md border bg-muted/20 p-3">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="text-muted-foreground">Bewertungsstand</span>
<span className="font-medium">{evaluatedSessionShare}% / {openSessionShare}%</span>
<span className="font-medium">{billedSessionShare}% / {nonBillableSessionShare}% / {openSessionShare}%</span>
</div>
<div className="flex h-2 overflow-hidden rounded-full bg-muted">
<div className="bg-violet-500 dark:bg-violet-400" style={{ width: `${evaluatedSessionShare}%` }} />
<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 className="grid gap-2 text-xs sm:grid-cols-2 xl:grid-cols-1 2xl:grid-cols-2">
<div className={evaluatedTextClass}>Bewertet: {evaluatedSessions} Session(s), {formatMinutes(evaluatedMinutes)}</div>
<div className={openTextClass}>Offen: {totals?.openSessions ?? 0} Session(s), {formatMinutes(totals?.openMinutes ?? 0)}</div>
<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>
@@ -586,7 +593,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<DeltaBadge minutes={totals?.crmDeltaMinutes ?? 0} />
</div>
<TimeComparison trackedMinutes={totals?.minutes ?? 0} crmMinutes={totals?.crmBilledMinutes ?? 0} sessions={totals?.sessions ?? 0} />
<div className="text-xs text-muted-foreground">Teamspace-Abdeckung: {crmCoverage}% der getrackten Session-Zeit</div>
<div className="text-xs text-muted-foreground">Teamspace-Abdeckung: {crmCoverage}% der abgerechneten Zeit</div>
</div>
<div className="grid grid-cols-2 gap-2 text-sm">
@@ -614,8 +621,9 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<p className="text-muted-foreground">Top Organisation</p>
<p className="truncate font-semibold">{topOrganization.organization_name}</p>
<p className="flex flex-wrap gap-x-3 gap-y-1 text-xs">
<span className={trackedTextClass}>Sessions {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={nonBillableTextClass}>Nicht {formatMinutes(topOrganization.non_billable_minutes)}</span>
</p>
</div>
) : null}
@@ -628,7 +636,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<CardHeader className="gap-3 p-4 sm:flex-row sm:items-start sm:justify-between sm:space-y-0">
<div>
<CardTitle>Statistik pro Organisation</CardTitle>
<CardDescription>Kundenaufwand inklusive Teamspace-Differenz und Anteil am getrackten Monatsaufwand.</CardDescription>
<CardDescription>Kundenaufwand inklusive Teamspace-Differenz und Anteil an der abgerechneten Monatszeit.</CardDescription>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline">
@@ -646,7 +654,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
<TableHead>Sessions</TableHead>
<TableHead>Zeiten</TableHead>
<TableHead>Differenz</TableHead>
<TableHead>Anteil am Monat</TableHead>
<TableHead>Anteil abgerechnet</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -673,8 +681,9 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
</div>
<Progress value={percentage(organization.total_minutes, maxOrganizationMinutes)} />
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs">
<span className={trackedTextClass}>Sessions {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={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>
@@ -742,7 +751,7 @@ export function StatisticsPage({ onNavigate }: StatisticsPageProps) {
</div>
<Progress value={percentage(group.total_minutes, totals?.minutes ?? 0)} />
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs">
<span className={trackedTextClass}>Sessions {formatMinutes(group.total_minutes)}</span>
<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-3 gap-2 text-xs text-muted-foreground">
+17 -5
View File
@@ -206,22 +206,32 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate }: T
const savedDayBillingInput = (day: string) => formatHoursInput(dayBillingMap.get(day));
const isDayBillingDirty = (day: string) => (dayBillingInputs[day] ?? "") !== savedDayBillingInput(day);
const sessionGroups = useMemo(() => {
const groups = new Map<string, { key: string; label: string; totalMinutes: number; sessions: SessionEntry[] }>();
const groups = new Map<
string,
{ key: string; label: string; totalMinutes: number; nonBillableMinutes: number; openMinutes: number; sessions: SessionEntry[] }
>();
for (const session of data?.sessions ?? []) {
const key = sessionDayKey(session.started_at);
const group = groups.get(key);
const billedMinutes = session.billing_status === "billed" ? session.rounded_minutes : 0;
const nonBillableMinutes = session.billing_status === "non_billable" ? session.rounded_minutes : 0;
const openMinutes = session.billing_status === null ? session.rounded_minutes : 0;
if (group) {
group.sessions.push(session);
group.totalMinutes += session.rounded_minutes;
group.totalMinutes += billedMinutes;
group.nonBillableMinutes += nonBillableMinutes;
group.openMinutes += openMinutes;
continue;
}
groups.set(key, {
key,
label: formatDate(session.started_at),
totalMinutes: session.rounded_minutes,
totalMinutes: billedMinutes,
nonBillableMinutes,
openMinutes,
sessions: [session]
});
}
@@ -551,7 +561,7 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate }: T
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-2 text-sm">
<span className="text-muted-foreground">{periodLabel} gesamt</span>
<span className="text-muted-foreground">{periodLabel} abgerechnet</span>
<span className="font-semibold">{formatMinutes(periodTotalMinutes)}</span>
</div>
<div className="flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-2 text-sm">
@@ -568,7 +578,9 @@ export function TicketDetailPage({ periodType, period, ticketId, onNavigate }: T
<div className="flex flex-col gap-2 border-b bg-muted/40 px-3 py-2 sm:flex-row sm:items-center sm:justify-between">
<p className="font-medium">{group.label}</p>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline">Tag gesamt {formatMinutes(group.totalMinutes)}</Badge>
<Badge variant="outline">Tag abgerechnet {formatMinutes(group.totalMinutes)}</Badge>
{group.nonBillableMinutes > 0 ? <Badge variant="secondary">Nicht {formatMinutes(group.nonBillableMinutes)}</Badge> : null}
{group.openMinutes > 0 ? <Badge variant="warning">Offen {formatMinutes(group.openMinutes)}</Badge> : null}
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<label htmlFor={`crm-billing-${group.key}`}>Teamspace</label>
<Input