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 };