'use client'; import dynamic from 'next/dynamic'; import clsx from 'clsx'; import { BanknotesIcon, ShoppingBagIcon, ArrowTrendingUpIcon, ArrowTrendingDownIcon, ScaleIcon, EyeSlashIcon, ExclamationTriangleIcon, CheckCircleIcon, ChevronDownIcon, PlusIcon, } from '@heroicons/react/24/outline'; import { useLanguage } from '@/context/LanguageContext'; import { usePrivacy } from '@/context/PrivacyContext'; import { useMoney } from '@/lib/useMoney'; import type { EventFinance } from '@/lib/api'; import Button from '@/components/ui/Button'; import { Skeleton } from '@/components/ui/Skeleton'; import { Money, EmptyState, Pill } from './ui'; import { breakEvenState, detailChartsAvailable } from './derive'; import type { FinanceSection } from './FinanceTab'; // Recharts only loads when someone opens the Finance tab. const FinanceCharts = dynamic(() => import('./FinanceCharts'), { ssr: false, loading: () => , }); function Kpi({ label, tag, value, detail, icon: Icon, tone, children }: { label: string; tag?: React.ReactNode; value: React.ReactNode; detail?: React.ReactNode; icon: typeof BanknotesIcon; tone: 'blue' | 'orange' | 'green' | 'red' | 'purple' | 'gray'; children?: React.ReactNode; }) { const tones = { blue: 'bg-blue-50 text-blue-600', orange: 'bg-orange-50 text-orange-600', green: 'bg-green-50 text-green-600', red: 'bg-red-50 text-red-600', purple: 'bg-purple-50 text-purple-600', gray: 'bg-gray-100 text-gray-600', }; return (

{label}

{tag}
{value}
{detail &&
{detail}
} {children}
); } /** The P&L as a compact statement under the waterfall, in the same order as its bars. */ function Breakdown({ data, finalized }: { data: EventFinance; finalized: boolean }) { const { t, locale } = useLanguage(); const { num } = useMoney(); const s = data.summary; const catName = (id: string | null) => { const c = data.categories.find((x) => x.id === id); return c ? (locale === 'es' ? c.nameEs : c.nameEn) : t('admin.finance.charts.uncategorized'); }; const resultTone = (v: number | null) => (finalized && v !== null ? (v < 0 ? 'text-red-600' : 'text-green-700') : undefined); const Row = ({ label, value, sign, strong, muted, className }: { label: React.ReactNode; value: number | null; sign?: '+' | '−'; strong?: boolean; muted?: boolean; className?: string; }) => (
{sign && {sign}}{label}
); const partners = s.split.partners; const showSplit = partners.length > 0 && s.split.organization !== null; return (

{t('admin.finance.statement.title')}

{s.revenue.refunds > 0 && } {s.revenue.fees > 0 && } {s.expenses.total > 0 && (
− {t('admin.finance.waterfall.expenses')}
{s.expenses.byCategory.map((c) => (
{catName(c.categoryId)}
))}
)} {s.revenue.otherIncome > 0 && }
{showSplit && ( <> {partners.map((p) => ( {p.name}{p.share < 0 && {t('admin.finance.statement.carriesLoss')}}} value={Math.abs(p.share)} muted /> ))}
)}
); } export function SummaryView({ data, goTo }: { data: EventFinance; goTo: (s: FinanceSection) => void }) { const { t, locale } = useLanguage(); const { privacyMode } = usePrivacy(); const { num } = useMoney(); const s = data.summary; const finalized = data.status !== 'open'; const loss = s.profit < 0; const capacity = data.event.capacity; const canAdd = data.status === 'open' && (data.viewer.canEditExpenses || data.viewer.canEditOwnExpenses); const nothingYet = s.revenue.gross === 0 && s.expenses.total === 0 && s.revenue.otherIncome === 0; if (nothingYet) { return ( } title={t('admin.finance.empty.title')} body={t('admin.finance.empty.body')}> {canAdd && } ); } const sold = s.counts.ticketsSold; const be = breakEvenState(s); let breakEvenValue: React.ReactNode; let breakEvenDetail: React.ReactNode; let breakEvenBar: number | null = null; if (be.kind === 'noCosts') { breakEvenValue = {t('admin.finance.breakEven.noCosts')}; breakEvenDetail = t('admin.finance.breakEven.noCostsHint'); } else if (be.kind === 'unreachable') { breakEvenValue = {t('admin.finance.kpi.breakEvenNever')}; } else if (privacyMode) { breakEvenValue = '••••'; } else if (be.kind === 'reached') { breakEvenValue = ( {be.at === 0 ? t('admin.finance.breakEven.coveredWithoutTickets') : t('admin.finance.breakEven.reached', { at: num(be.at), sold: num(be.sold) })} ); } else { breakEvenValue = {t('admin.finance.breakEven.needed', { count: num(be.remaining) })}; breakEvenBar = be.pct; breakEvenDetail = be.needed > capacity ? {t('admin.finance.kpi.beyondCapacity', { capacity: num(capacity) })} : t('admin.finance.kpi.breakEvenProgress', { sold: num(be.sold), total: num(be.needed) }); } const show = detailChartsAvailable(s); return (
} detail={privacyMode ? undefined : ( <>

{t('admin.finance.kpi.revenueDetail', { count: num(sold) })}

{s.revenue.gross > 0 && (

{t('admin.finance.kpi.presale')} · {t('admin.finance.kpi.door')}

)} )} icon={BanknotesIcon} tone="blue" /> } detail={privacyMode || s.expenses.total === 0 ? undefined : ( {t('admin.finance.charts.paid')} · {t('admin.finance.charts.planned')} )} icon={ShoppingBagIcon} tone="orange" /> {t('admin.finance.kpi.estimated')}} value={} detail={t(finalized ? 'admin.finance.kpi.afterFees' : 'admin.finance.kpi.estimatedHint')} icon={loss ? ArrowTrendingDownIcon : ArrowTrendingUpIcon} tone={finalized ? (loss ? 'red' : 'green') : 'gray'} /> {breakEvenBar !== null && (
)}
{privacyMode ? (
{t('admin.finance.hiddenStats')}
) : ( <>
{!show.costs && (

{canAdd ? ( ) : t('admin.finance.charts.addExpensesHint')}

)} )}
); }