import React, { useMemo, useState } from 'react'; import { Badge, BudgetBar, Card, EmptyState, ErrorBox, Segmented, Spinner, inputClass } from '../ui/controls'; import { Icon } from '../ui/Icon'; import { CountUp, ProgressChart } from '../ui/charts'; import { ReportTable } from './ReportTable'; import { ExportMenu } from './ExportMenu'; import { useAuth } from '../../lib/auth'; import { projectsApi, qs, reportsApi, type ProjectDetail } from '../../lib/api'; import { useLoader } from '../../lib/hooks'; import { formatDate, formatEuro, formatHours, formatNumber, mondayOf, todayIso } from '../../lib/format'; import { useRouter } from '../../lib/router'; import { TYPE_TONE } from '../projects/Projects'; import { PROJECT_TYPE_LABELS } from '../../types'; export const ProjectReport: React.FC<{ id: number | null }> = ({ id }) => { const { navigate } = useRouter(); const projects = useLoader(() => projectsApi.list(), []); const report = useLoader(() => (id ? reportsApi.project(id) : Promise.resolve(null)), [id]); const options = useMemo(() => [...(projects.data ?? [])].sort((a, b) => a.display_name.localeCompare(b.display_name, 'de')), [projects.data]); return (
{!id ? ( ) : report.error ? ( ) : !report.data ? ( ) : ( )}
); }; export const ProjectReportBody: React.FC<{ detail: ProjectDetail; compact?: boolean }> = ({ detail, compact }) => { const { settings } = useAuth(); const { navigate } = useRouter(); const [chart, setChart] = useState<'progress' | 'weekly'>('progress'); const [tab, setTab] = useState<'tasks' | 'team' | 'pots'>('tasks'); const [showNotes, setShowNotes] = useState(false); const p = detail.project; const t = detail.totals; const budgetHours = p.budget_hours; const remaining = budgetHours != null ? Math.max(0, budgetHours - p.budget_used_minutes / 60) : null; const remainingPct = budgetHours ? Math.round(((remaining ?? 0) / budgetHours) * 100) : null; const thisWeek = mondayOf(todayIso()); const virtualRate = p.type === 'fixed' && p.used_minutes ? (p.budget_value ?? 0) / (p.used_minutes / 60) : null; const month = todayIso().slice(0, 7); const exportOptions = [ { label: 'PDF – Stundennachweis (Monat)', hint: 'Alle Mitarbeiter, aktueller Monat', path: `/exports/timesheet${qs({ month, format: 'pdf', user_id: 'all', project_id: p.id })}`, filename: `${p.code}.pdf` }, { label: 'Excel – Stundennachweis (Monat)', path: `/exports/timesheet${qs({ month, format: 'xlsx', user_id: 'all', project_id: p.id, amounts: 1 })}`, filename: `${p.code}.xlsx` }, { label: 'CSV – alle Buchungen', path: `/exports/report${qs({ from: p.start_date, to: todayIso(), project_id: p.id, group: 'detail', format: 'csv', include_fixed: 1 })}`, filename: `${p.code}.csv` }, { label: 'PDF – Tasks gesamt', path: `/exports/report${qs({ from: p.start_date, to: todayIso(), project_id: p.id, group: 'tasks', format: 'pdf', include_fixed: 1 })}`, filename: `${p.code}-tasks.pdf` }, ]; return (
{!compact && (

[{p.code}] {p.display_name}

{PROJECT_TYPE_LABELS[p.type]} Start: {formatDate(p.start_date)} {p.end_date && ` · Ende: ${formatDate(p.end_date)}`}
{p.notes && (

{p.notes}

)}
)}
Stunden · Start {formatDate(p.start_date)}
`${formatNumber(v, 0)} h`} points={detail.weekly.map((w) => ({ label: w.label, value: w.cumulative_minutes == null ? null : w.cumulative_minutes / 60, bar: w.billable_minutes / 60, barB: (w.minutes - w.billable_minutes) / 60, current: w.week_start === thisWeek, }))} />
formatNumber(v, 2)} className="text-3xl font-extrabold text-ink-900" />

{remaining == null ? '∞' : formatNumber(remaining, 2)}

{p.type === 'fixed' ? ( <>

{virtualRate ? formatEuro(virtualRate, 2) : 'n/a'}

) : p.type === 'abruf' ? ( <>

{formatEuro(p.rate, 2)}

) : ( <>

{remaining == null ? '∞' : formatNumber(remaining / settings.hours_per_day, 1)}

)}
formatEuro(v, 2)} className="text-3xl font-extrabold text-ink-900" /> formatEuro(v, 2)} className="text-3xl font-extrabold text-ink-900" /> x.status === 'active').length} von ${p.pots.length}`} />
{( [ ['tasks', 'Tasks'], ['team', 'Team'], ['pots', 'Budgettöpfe'], ] as const ).map(([key, label]) => ( ))}
{tab === 'tasks' && } {tab === 'team' && } {tab === 'pots' && }
); }; /** Gesamtuebersicht aller Toepfe und darunter jeder Topf einzeln. */ const PotsBreakdown: React.FC<{ detail: ProjectDetail }> = ({ detail }) => { const { settings } = useAuth(); const p = detail.project; const totalBudget = p.pots.reduce((s, x) => s + (x.budget_hours ?? 0), 0); return (

Gesamt · {p.pots.length} Töpfe

{detail.pots.map((pot, i) => (

{pot.label}

{formatDate(pot.valid_from)} – {formatDate(pot.valid_to)} · {pot.status === 'active' ? 'aktiv' : pot.status === 'expired' ? 'abgelaufen' : pot.status === 'exhausted' ? 'aufgebraucht' : 'geplant'}

))}
); }; const Kpi: React.FC<{ title: string; index: number; children: React.ReactNode }> = ({ title, index, children }) => (

{title}

{children}
); const Row: React.FC<{ label: string; value: string }> = ({ label, value }) => (

{label} {value}

); export { Row as ReportRow, Kpi as ReportKpi };