import React, { useMemo, useState } from 'react'; import { Avatar } from '../ui/controls'; import { Icon } from '../ui/Icon'; import { SplitBar } from '../ui/charts'; import { colorFor, formatEuro, formatHours } from '../../lib/format'; import type { GroupRow } from '../../lib/api'; type SortKey = 'name' | 'minutes' | 'billable_minutes' | 'amount' | 'utilization'; /** * Tabelle wie im Harvest-Report: Name, Stunden mit Doppelbalken, Auslastung, * abrechenbare Stunden mit Anteil, Betrag. Spaltenkoepfe sortieren. */ export const ReportTable: React.FC<{ rows: GroupRow[]; label: string; kind: 'clients' | 'projects' | 'tasks' | 'users' | 'team'; showAmounts?: boolean; onRowClick?: (row: GroupRow) => void; totals?: boolean; }> = ({ rows, label, kind, showAmounts = true, onRowClick, totals = true }) => { const [sort, setSort] = useState<{ key: SortKey; dir: 1 | -1 }>({ key: kind === 'users' ? 'name' : 'minutes', dir: kind === 'users' ? 1 : -1 }); const sorted = useMemo( () => [...rows].sort((a, b) => { const av = sort.key === 'name' ? a.name : (a[sort.key] ?? -1); const bv = sort.key === 'name' ? b.name : (b[sort.key] ?? -1); return (typeof av === 'string' ? av.localeCompare(bv as string, 'de') : (av as number) - (bv as number)) * sort.dir; }), [rows, sort] ); const max = Math.max(1, ...rows.map((r) => r.minutes)); const showUtil = kind === 'users'; const sum = rows.reduce( (s, r) => ({ minutes: s.minutes + r.minutes, billable: s.billable + r.billable_minutes, amount: s.amount + r.amount }), { minutes: 0, billable: 0, amount: 0 } ); const head = (key: SortKey, text: string, align: 'left' | 'right' = 'right', className = '') => (
Keine Buchungen im Zeitraum.
; return (| {showUtil && head('utilization', 'Auslastung', 'right', 'w-[116px]')} {head('billable_minutes', 'Billable Std.', 'right', 'w-[160px]')} {showAmounts && head('amount', 'Billable Betrag', 'right', 'w-[150px] pr-4')} | |||||
|---|---|---|---|---|---|
|
{kind === 'users' || kind === 'team' ? (
|
{formatHours(r.minutes)} |
|
{showUtil && (
105 ? 'text-red-600' : r.utilization >= 75 ? 'text-brand-700' : 'text-ink-600'}`}> {r.utilization == null ? '–' : `${r.utilization} %`} | )}{formatHours(r.billable_minutes)} ({share} %) | {showAmounts &&{formatEuro(r.amount, 2)} | }
| Summe | {formatHours(sum.minutes)} | {showUtil && | } | {formatHours(sum.billable)} ({sum.minutes ? Math.round((sum.billable / sum.minutes) * 100) : 0} %) | {showAmounts &&{formatEuro(sum.amount, 2)} | }