'use client'; import { useCallback, useEffect, useState } from 'react'; import toast from 'react-hot-toast'; import clsx from 'clsx'; import { PlusIcon, PencilIcon, TrashIcon, DocumentArrowDownIcon, CheckCircleIcon, LockClosedIcon, LockOpenIcon, ClockIcon, XMarkIcon, ChevronDownIcon, UserGroupIcon, } from '@heroicons/react/24/outline'; import { useLanguage } from '@/context/LanguageContext'; import { usePrivacy } from '@/context/PrivacyContext'; import { useMoney } from '@/lib/useMoney'; import { financeApi, SHARE_TYPES, LOSS_RULES, type EventFinance, type EventPartner, type FinanceAuditEntry, type LossRule, type PartnerInput, type ShareType, } from '@/lib/api'; import Button from '@/components/ui/Button'; import { downloadBlob } from '../_utils/format'; import { Modal, Field, PygInput, DateInput, Money, ChoiceCards, EmptyState, Pill, inputClass, iconButtonClass, todayIso } from './ui'; import { UserSearch } from './UserSearch'; import { bpToPercent, percentToBp } from './format'; import { FinalizeDialog } from './FinalizeDialog'; import type { Lifecycle } from './derive'; type T = (key: string, params?: Record) => string; type Fmt = ReturnType; function dealText(p: EventPartner, t: T, m: Fmt): string { switch (p.shareType) { case 'percent_profit': return t('admin.finance.deal.percentProfit', { percent: m.pct(p.percentBp) }); case 'percent_revenue': return t('admin.finance.deal.percentRevenue', { percent: m.pct(p.percentBp) }); case 'fixed': return t('admin.finance.deal.fixed', { amount: m.pyg(p.fixedAmount) }); case 'fixed_plus_percent_above_threshold': return t('admin.finance.deal.fixedPlus', { amount: m.pyg(p.fixedAmount), percent: m.pct(p.percentBp), threshold: m.pyg(p.thresholdAmount) }); } } function ModalActions({ children }: { children: React.ReactNode }) { return
{children}
; } // ==================== Partner form ==================== interface PartnerForm { userId: string | null; userLabel: string; externalName: string; roleLabel: string; shareType: ShareType; percent: string; fixedAmount: number; thresholdAmount: number; lossRule: LossRule; lossCapAmount: number; } const emptyPartner = (): PartnerForm => ({ userId: null, userLabel: '', externalName: '', roleLabel: '', shareType: 'percent_profit', percent: '', fixedAmount: 0, thresholdAmount: 0, lossRule: 'none', lossCapAmount: 0, }); function PartnerModal({ open, onClose, eventId, partner, onSaved }: { open: boolean; onClose: () => void; eventId: string; partner: EventPartner | null; onSaved: () => void; }) { const { t } = useLanguage(); const [form, setForm] = useState(emptyPartner()); const [touched, setTouched] = useState(false); const [saving, setSaving] = useState(false); const set = (k: K, v: PartnerForm[K]) => setForm((f) => ({ ...f, [k]: v })); const search = useCallback((q: string) => financeApi.searchPartnerCandidates(eventId, q), [eventId]); useEffect(() => { if (!open) return; setTouched(false); setForm(partner ? { userId: partner.userId, userLabel: partner.userId ? partner.name : '', externalName: partner.externalName || '', roleLabel: partner.roleLabel || '', shareType: partner.shareType, percent: partner.percentBp ? bpToPercent(partner.percentBp) : '', fixedAmount: partner.fixedAmount, thresholdAmount: partner.thresholdAmount, lossRule: partner.lossRule, lossCapAmount: partner.lossCapAmount, } : emptyPartner()); }, [open, partner]); const st = form.shareType; const hasPercent = st !== 'fixed'; const hasFixed = st === 'fixed' || st === 'fixed_plus_percent_above_threshold'; const whoError = touched && !form.userId && !form.externalName.trim() ? t('admin.finance.split.needsNameOrUser') : undefined; const submit = async () => { setTouched(true); if (!form.userId && !form.externalName.trim()) return; setSaving(true); const payload: PartnerInput = { userId: form.userId, externalName: form.externalName.trim() || null, roleLabel: form.roleLabel.trim() || null, shareType: st, percentBp: hasPercent ? percentToBp(form.percent) : 0, fixedAmount: hasFixed ? form.fixedAmount : 0, thresholdAmount: st === 'fixed_plus_percent_above_threshold' ? form.thresholdAmount : 0, lossRule: st === 'percent_profit' ? form.lossRule : 'none', lossCapAmount: st === 'percent_profit' && form.lossRule === 'capped' ? form.lossCapAmount : 0, }; try { if (partner) await financeApi.updatePartner(eventId, partner.id, payload); else await financeApi.createPartner(eventId, payload); toast.success(t('admin.finance.split.partnerSaved')); onSaved(); onClose(); } catch (error: any) { toast.error(error.message); } finally { setSaving(false); } }; return (
{form.userId ? (
{form.userLabel}
) : ( setForm((f) => ({ ...f, userId: u.id, userLabel: `${u.name} (${u.email})` }))} placeholder={t('admin.finance.split.searchUser')} noResults={t('admin.team.noResults')} /> )} {!form.userId && ( set('externalName', e.target.value)} /> )}
set('roleLabel', e.target.value)} /> name="shareType" value={st} onChange={(v) => set('shareType', v)} options={SHARE_TYPES.map((s) => ({ key: s, title: t(`admin.finance.shareTypes.${s}`), description: t(`admin.finance.shareHints.${s}`) }))} />
{hasFixed && ( set('fixedAmount', n)} /> )} {hasPercent && (
set('percent', e.target.value)} /> %
)} {st === 'fixed_plus_percent_above_threshold' && ( set('thresholdAmount', n)} /> )}
{st === 'percent_profit' && ( name="lossRule" columns={3} value={form.lossRule} onChange={(v) => set('lossRule', v)} options={LOSS_RULES.map((r) => ({ key: r, title: t(`admin.finance.lossRuleTitles.${r}`), description: t(`admin.finance.lossRules.${r}`) }))} /> {form.lossRule === 'capped' && (
set('lossCapAmount', n)} />
)}
)}
); } // ==================== Payout ==================== function PayoutModal({ partner, amount, onClose, eventId, onSaved }: { partner: EventPartner | null; amount: number; onClose: () => void; eventId: string; onSaved: () => void }) { const { t } = useLanguage(); const [method, setMethod] = useState(''); const [date, setDate] = useState(''); const [note, setNote] = useState(''); const [saving, setSaving] = useState(false); useEffect(() => { if (partner) { setMethod(''); setDate(todayIso()); setNote(''); } }, [partner]); const submit = async () => { if (!partner) return; setSaving(true); try { await financeApi.markPartnerPaid(eventId, partner.id, { paid: true, payoutMethod: method || null, payoutDate: date || null, payoutNote: note || null }); onSaved(); onClose(); } catch (error: any) { toast.error(error.message); } finally { setSaving(false); } }; return (
{t(amount < 0 ? 'admin.finance.split.owes' : 'admin.finance.split.payout')}
setMethod(e.target.value)} placeholder={t('admin.finance.split.payoutMethodPlaceholder')} />