import React, { useState, useCallback } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { AppLayout } from '../../components/layout/AppLayout';
import { PageLoader } from '../../components/ui/LoadingSpinner';
import {
CreditCard,
Handshake,
Calculator,
DollarSign,
Search,
Plus,
X,
Loader2,
CheckCircle2,
AlertTriangle,
Clock,
Banknote,
ArrowUpDown,
ChevronDown,
ChevronUp,
Eye,
EyeOff,
Trash2,
Pencil,
Send,
Calendar,
Percent,
} from 'lucide-react';
// ─── Types ───────────────────────────────────────────────────────────────
interface Partnership {
id: number;
partner_id: number;
partner_name: string;
partner_email: string;
company_id: number;
company_name: string;
commission_rate: number;
revenue_source: string;
effective_date: string | null;
created_at: string | null;
}
interface CommissionRecord {
id: number;
partner_commission_id: number;
partner_name: string;
company_name: string;
period: string;
revenue_amount: number;
commission_rate: number;
commission_amount: number;
status: string;
revenue_confidence: string;
calculated_at: string | null;
}
interface Payout {
id: number;
partner_id: number;
partner_name: string;
amount: number;
currency: string;
status: string;
scheduled_date: string | null;
paid_date: string | null;
payment_method: string;
transaction_id: string | null;
record_count: number;
created_at: string | null;
}
interface CalculateResult {
period: string;
created: number;
updated: number;
partnerships_processed: number;
}
// ─── API helpers ─────────────────────────────────────────────────────────
const csrf = async (): Promise<string> => {
const res = await fetch('/api/csrf-token', { credentials: 'include' });
const data = await res.json();
return data.csrf_token;
};
const adminFetch = async (url: string, opts?: RequestInit) => {
const res = await fetch(url, {
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
...opts,
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || `HTTP ${res.status}`);
}
return res.json();
};
// ─── Tab types ───────────────────────────────────────────────────────────
type TabType = 'partnerships' | 'records' | 'payouts';
// ─── Status badge helper ────────────────────────────────────────────────
function StatusBadge({ status }: { status: string }) {
const map: Record<string, { bg: string; text: string; label: string }> = {
active: { bg: 'bg-green-100', text: 'text-green-700', label: 'Active' },
terminated: { bg: 'bg-red-100', text: 'text-red-700', label: 'Terminated' },
calculated: { bg: 'bg-blue-100', text: 'text-blue-700', label: 'Calculated' },
approved: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'Approved' },
paid: { bg: 'bg-green-100', text: 'text-green-700', label: 'Paid' },
disputed: { bg: 'bg-red-100', text: 'text-red-700', label: 'Disputed' },
scheduled: { bg: 'bg-purple-100', text: 'text-purple-700', label: 'Scheduled' },
completed: { bg: 'bg-green-100', text: 'text-green-700', label: 'Completed' },
failed: { bg: 'bg-red-100', text: 'text-red-700', label: 'Failed' },
};
const cfg = map[status] || { bg: 'bg-gray-100', text: 'text-gray-600', label: status };
return (
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${cfg.bg} ${cfg.text}`}>
{cfg.label}
</span>
);
}
function ConfidenceBadge({ confidence }: { confidence: string }) {
const map: Record<string, { bg: string; text: string; label: string }> = {
verified: { bg: 'bg-green-50', text: 'text-green-600', label: '✓ Verified' },
self_reported: { bg: 'bg-amber-50', text: 'text-amber-600', label: '~ Self-reported' },
estimated: { bg: 'bg-gray-100', text: 'text-gray-500', label: '? Estimated' },
};
const cfg = map[confidence] || map.estimated;
return (
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${cfg.bg} ${cfg.text}`}>
{cfg.label}
</span>
);
}
function formatCurrency(amount: number, currency = 'USD') {
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
}
function formatPercent(rate: number) {
return `${(rate * 100).toFixed(1)}%`;
}
// ─── Main Page ──────────────────────────────────────────────────────────
export function CommissionsAdminPage() {
const [activeTab, setActiveTab] = useState<TabType>('partnerships');
const [search, setSearch] = useState('');
const [showCreateModal, setShowCreateModal] = useState(false);
const [showCalculateModal, setShowCalculateModal] = useState(false);
const [showPayoutModal, setShowPayoutModal] = useState(false);
const [showTerminateModal, setShowTerminateModal] = useState<number | null>(null);
const [showUpdateRateModal, setShowUpdateRateModal] = useState<number | null>(null);
const [newRate, setNewRate] = useState('');
const [selectedPartnerId, setSelectedPartnerId] = useState<number | null>(null);
const queryClient = useQueryClient();
// ── Partnerships ──
const { data: partnerships, isLoading: loadingPartnerships } = useQuery<Partnership[]>({
queryKey: ['admin-commissions-partnerships'],
queryFn: () => adminFetch('/api/super-admin/commissions/partnerships').then(d => d.items),
});
// ── Records ──
const { data: records, isLoading: loadingRecords } = useQuery<CommissionRecord[]>({
queryKey: ['admin-commissions-records'],
queryFn: () => adminFetch('/api/super-admin/commissions/records').then(d => d.items),
});
// ── Payouts ──
const { data: payouts, isLoading: loadingPayouts } = useQuery<Payout[]>({
queryKey: ['admin-commissions-payouts'],
queryFn: () => adminFetch('/api/super-admin/commissions/payouts').then(d => d.items),
});
// ── Unique partners for payout dropdown ──
const { data: partnersList } = useQuery<Partnership[]>({
queryKey: ['admin-commissions-partnerships'],
queryFn: () => adminFetch('/api/super-admin/commissions/partnerships').then(d => d.items),
});
// ── Create partnership mutation ──
const createPartnership = useMutation({
mutationFn: async (data: { partner_email: string; company_id: number; commission_rate: number; revenue_source: string }) => {
const token = await csrf();
return adminFetch('/api/super-admin/commissions/partnerships', {
method: 'POST',
headers: { 'X-CSRFToken': token },
body: JSON.stringify(data),
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-commissions-partnerships'] });
setShowCreateModal(false);
},
});
// ── Update rate mutation ──
const updateRate = useMutation({
mutationFn: async ({ id, rate }: { id: number; rate: number }) => {
const token = await csrf();
return adminFetch(`/api/super-admin/commissions/partnerships/${id}`, {
method: 'PUT',
headers: { 'X-CSRFToken': token },
body: JSON.stringify({ commission_rate: rate }),
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-commissions-partnerships'] });
setShowUpdateRateModal(null);
setNewRate('');
},
});
// ── Terminate mutation ──
const terminatePartnership = useMutation({
mutationFn: async (id: number) => {
const token = await csrf();
return adminFetch(`/api/super-admin/commissions/partnerships/${id}`, {
method: 'PUT',
headers: { 'X-CSRFToken': token },
body: JSON.stringify({ terminate: true }),
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-commissions-partnerships'] });
setShowTerminateModal(null);
},
});
// ── Approve record mutation ──
const approveRecord = useMutation({
mutationFn: async (id: number) => {
const token = await csrf();
return adminFetch(`/api/super-admin/commissions/records/${id}/approve`, {
method: 'POST',
headers: { 'X-CSRFToken': token },
body: JSON.stringify({}),
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-commissions-records'] });
},
});
// ── Calculate mutation ──
const calculateCommissions = useMutation({
mutationFn: async (period: string) => {
const token = await csrf();
return adminFetch('/api/super-admin/commissions/calculate', {
method: 'POST',
headers: { 'X-CSRFToken': token },
body: JSON.stringify({ period }),
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-commissions-records'] });
queryClient.invalidateQueries({ queryKey: ['admin-commissions-partnerships'] });
setShowCalculateModal(false);
},
});
// ── Create payout mutation ──
const createPayout = useMutation({
mutationFn: async (data: { partner_id: number; payment_method?: string; notes?: string }) => {
const token = await csrf();
return adminFetch('/api/super-admin/commissions/payouts', {
method: 'POST',
headers: { 'X-CSRFToken': token },
body: JSON.stringify(data),
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-commissions-payouts'] });
queryClient.invalidateQueries({ queryKey: ['admin-commissions-records'] });
setShowPayoutModal(false);
setSelectedPartnerId(null);
},
});
// ── Complete payout mutation ──
const completePayout = useMutation({
mutationFn: async (id: number) => {
const token = await csrf();
return adminFetch(`/api/super-admin/commissions/payouts/${id}`, {
method: 'PUT',
headers: { 'X-CSRFToken': token },
body: JSON.stringify({ status: 'completed' }),
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-commissions-payouts'] });
},
});
// ── Stats ──
const totalActive = (partnerships ?? []).filter(p => p.status === 'active' || true).length;
const totalRevenue = (records ?? []).reduce((sum, r) => sum + r.revenue_amount, 0);
const totalCommission = (records ?? []).reduce((sum, r) => sum + r.commission_amount, 0);
const pendingPayout = (records ?? []).filter(r => r.status === 'approved').reduce((sum, r) => sum + r.commission_amount, 0);
const tabs = [
{ id: 'partnerships' as const, label: 'Partnerships', count: partnerships?.length ?? 0, icon: Handshake },
{ id: 'records' as const, label: 'Records', count: records?.length ?? 0, icon: Calculator },
{ id: 'payouts' as const, label: 'Payouts', count: payouts?.length ?? 0, icon: Banknote },
];
if (loadingPartnerships && loadingRecords && loadingPayouts) {
return (
<AppLayout title="Commissions">
<PageLoader />
</AppLayout>
);
}
return (
<AppLayout title="Commissions">
{/* Header */}
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-xl sm:text-2xl font-bold text-surface-900">Commissions</h1>
<p className="mt-1 text-sm text-surface-500">
Manage partner commission agreements, calculations, and payouts
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<button
onClick={() => setShowCalculateModal(true)}
disabled={calculateCommissions.isPending}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-amber-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-amber-700 min-h-[44px] w-full sm:w-auto disabled:opacity-50"
>
{calculateCommissions.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Calculator className="h-4 w-4" />}
Calculate
</button>
<button
onClick={() => setShowCreateModal(true)}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-700 min-h-[44px] w-full sm:w-auto"
>
<Plus className="h-4 w-4" />
New Partnership
</button>
</div>
</div>
{/* Stats Row */}
<div className="mb-6 grid grid-cols-2 gap-4 lg:grid-cols-4">
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
<Handshake className="h-5 w-5 text-blue-600" />
</div>
<div>
<p className="text-xs font-medium text-surface-500">Partnerships</p>
<p className="text-xl font-bold text-surface-900">{totalActive}</p>
</div>
</div>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-green-100">
<DollarSign className="h-5 w-5 text-green-600" />
</div>
<div>
<p className="text-xs font-medium text-surface-500">Total Revenue</p>
<p className="text-xl font-bold text-surface-900">{formatCurrency(totalRevenue)}</p>
</div>
</div>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-100">
<Percent className="h-5 w-5 text-purple-600" />
</div>
<div>
<p className="text-xs font-medium text-surface-500">Total Commissions</p>
<p className="text-xl font-bold text-surface-900">{formatCurrency(totalCommission)}</p>
</div>
</div>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-amber-100">
<Clock className="h-5 w-5 text-amber-600" />
</div>
<div>
<p className="text-xs font-medium text-surface-500">Pending Payout</p>
<p className="text-xl font-bold text-surface-900">{formatCurrency(pendingPayout)}</p>
</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="mb-4 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex gap-1 rounded-lg bg-surface-100 p-1">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors min-h-[38px] ${
activeTab === tab.id
? 'bg-white text-surface-900 shadow-sm'
: 'text-surface-500 hover:text-surface-700'
}`}
>
<tab.icon className="h-4 w-4" />
{tab.label}
<span className={`ml-1 rounded-full px-1.5 py-0.5 text-xs ${
activeTab === tab.id ? 'bg-surface-200 text-surface-600' : 'bg-surface-200 text-surface-500'
}`}>
{tab.count}
</span>
</button>
))}
</div>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-surface-500" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={`Search ${activeTab}...`}
className="w-full border border-surface-300 pl-9 pr-3 py-2.5 text-sm rounded-lg outline-none focus:border-brand-500 min-h-[44px] w-full sm:w-56"
/>
</div>
</div>
{/* ─── Partnerships Tab ─── */}
{activeTab === 'partnerships' && (
<div className="overflow-x-auto rounded-xl border border-surface-200 bg-white">
{(partnerships ?? []).length === 0 ? (
<div className="flex flex-col items-center justify-center py-16">
<Handshake className="h-12 w-12 text-surface-300" />
<p className="mt-4 text-sm font-medium text-surface-700">No partnerships yet</p>
<p className="mt-1 text-xs text-surface-500">Create a partnership to start tracking commissions</p>
<button
onClick={() => setShowCreateModal(true)}
className="mt-4 inline-flex items-center gap-2 rounded-lg bg-brand-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-700 min-h-[44px]"
>
<Plus className="h-4 w-4" />
Create First Partnership
</button>
</div>
) : (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-surface-200 bg-surface-50">
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500 uppercase">Partner</th>
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500 uppercase">Company</th>
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500 uppercase">Rate</th>
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500 uppercase">Source</th>
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500 uppercase">Effective</th>
<th className="px-4 py-3 text-right text-xs font-medium text-surface-500 uppercase">Actions</th>
</tr>
</thead>
<tbody>
{partnerships?.map(p => (
<tr key={p.id} className="border-b border-surface-100 hover:bg-surface-50">
<td className="px-4 py-3">
<div className="font-medium text-surface-900">{p.partner_name}</div>
<div className="text-xs text-surface-500">{p.partner_email}</div>
</td>
<td className="px-4 py-3 text-surface-700">{p.company_name}</td>
<td className="px-4 py-3">
<span className="font-mono text-sm font-medium">{formatPercent(p.commission_rate)}</span>
</td>
<td className="px-4 py-3">
<span className="text-xs capitalize text-surface-500">{p.revenue_source.replace('_', ' ')}</span>
</td>
<td className="px-4 py-3 text-xs text-surface-500">
{p.effective_date ? new Date(p.effective_date).toLocaleDateString() : '—'}
</td>
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-1">
<button
onClick={() => {
setShowUpdateRateModal(p.id);
setNewRate((p.commission_rate * 100).toString());
}}
className="p-1.5 rounded hover:bg-surface-200 text-surface-500"
title="Update rate"
>
<Pencil className="h-3.5 w-3.5" />
</button>
<button
onClick={() => setShowTerminateModal(p.id)}
className="p-1.5 rounded hover:bg-red-100 text-surface-400 hover:text-red-600"
title="Terminate"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
{/* ─── Records Tab ─── */}
{activeTab === 'records' && (
<div className="overflow-x-auto rounded-xl border border-surface-200 bg-white">
{(records ?? []).length === 0 ? (
<div className="flex flex-col items-center justify-center py-16">
<Calculator className="h-12 w-12 text-surface-300" />
<p className="mt-4 text-sm font-medium text-surface-700">No records yet</p>
<p className="mt-1 text-xs text-surface-500">Run commission calculation to generate records</p>
<button
onClick={() => setShowCalculateModal(true)}
className="mt-4 inline-flex items-center gap-2 rounded-lg bg-amber-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-amber-700 min-h-[44px]"
>
<Calculator className="h-4 w-4" />
Calculate Commissions
</button>
</div>
) : (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-surface-200 bg-surface-50">
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500 uppercase">Period</th>
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500 uppercase">Partner</th>
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500 uppercase">Company</th>
<th className="px-4 py-3 text-right text-xs font-medium text-surface-500 uppercase">Revenue</th>
<th className="px-4 py-3 text-right text-xs font-medium text-surface-500 uppercase">Rate</th>
<th className="px-4 py-3 text-right text-xs font-medium text-surface-500 uppercase">Commission</th>
<th className="px-4 py-3 text-center text-xs font-medium text-surface-500 uppercase">Confidence</th>
<th className="px-4 py-3 text-center text-xs font-medium text-surface-500 uppercase">Status</th>
<th className="px-4 py-3 text-right text-xs font-medium text-surface-500 uppercase">Actions</th>
</tr>
</thead>
<tbody>
{records
?.filter(r =>
r.partner_name.toLowerCase().includes(search.toLowerCase()) ||
r.company_name.toLowerCase().includes(search.toLowerCase()) ||
r.period.includes(search)
)
.map(r => (
<tr key={r.id} className="border-b border-surface-100 hover:bg-surface-50">
<td className="px-4 py-3 font-mono text-sm">{r.period}</td>
<td className="px-4 py-3 text-surface-700">{r.partner_name}</td>
<td className="px-4 py-3 text-surface-700">{r.company_name}</td>
<td className="px-4 py-3 text-right font-mono text-sm">{formatCurrency(r.revenue_amount)}</td>
<td className="px-4 py-3 text-right font-mono text-sm">{formatPercent(r.commission_rate)}</td>
<td className="px-4 py-3 text-right font-mono text-sm font-medium text-green-700">
{formatCurrency(r.commission_amount)}
</td>
<td className="px-4 py-3 text-center">
<ConfidenceBadge confidence={r.revenue_confidence} />
</td>
<td className="px-4 py-3 text-center">
<StatusBadge status={r.status} />
</td>
<td className="px-4 py-3 text-right">
{r.status === 'calculated' && (
<button
onClick={() => approveRecord.mutate(r.id)}
disabled={approveRecord.isPending}
className="inline-flex items-center gap-1 rounded bg-green-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-green-700 min-h-[32px]"
>
{approveRecord.isPending ? <Loader2 className="h-3 w-3 animate-spin" /> : <CheckCircle2 className="h-3 w-3" />}
Approve
</button>
)}
{r.status !== 'calculated' && (
<span className="text-xs text-surface-400">—</span>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
{/* ─── Payouts Tab ─── */}
{activeTab === 'payouts' && (
<div className="space-y-4">
<div className="flex justify-end">
<button
onClick={() => setShowPayoutModal(true)}
disabled={!partnersList?.length}
className="inline-flex items-center gap-2 rounded-lg bg-brand-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-700 min-h-[44px] disabled:opacity-50"
>
<Send className="h-4 w-4" />
Create Payout
</button>
</div>
{(payouts ?? []).length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-xl border border-surface-200 bg-white py-16">
<Banknote className="h-12 w-12 text-surface-300" />
<p className="mt-4 text-sm font-medium text-surface-700">No payouts yet</p>
<p className="mt-1 text-xs text-surface-500">Approve commission records, then create a payout</p>
</div>
) : (
<div className="overflow-x-auto rounded-xl border border-surface-200 bg-white">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-surface-200 bg-surface-50">
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500 uppercase">Partner</th>
<th className="px-4 py-3 text-right text-xs font-medium text-surface-500 uppercase">Amount</th>
<th className="px-4 py-3 text-center text-xs font-medium text-surface-500 uppercase">Records</th>
<th className="px-4 py-3 text-center text-xs font-medium text-surface-500 uppercase">Method</th>
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500 uppercase">Scheduled</th>
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500 uppercase">Paid</th>
<th className="px-4 py-3 text-center text-xs font-medium text-surface-500 uppercase">Status</th>
<th className="px-4 py-3 text-right text-xs font-medium text-surface-500 uppercase">Actions</th>
</tr>
</thead>
<tbody>
{payouts
?.filter(p =>
p.partner_name.toLowerCase().includes(search.toLowerCase())
)
.map(p => (
<tr key={p.id} className="border-b border-surface-100 hover:bg-surface-50">
<td className="px-4 py-3 text-surface-700">{p.partner_name}</td>
<td className="px-4 py-3 text-right font-mono text-sm font-medium">
{formatCurrency(p.amount, p.currency)}
</td>
<td className="px-4 py-3 text-center">
<span className="inline-flex items-center justify-center h-6 w-6 rounded-full bg-surface-100 text-xs font-medium">
{p.record_count}
</span>
</td>
<td className="px-4 py-3 text-center text-xs capitalize text-surface-500">
{p.payment_method.replace('_', ' ')}
</td>
<td className="px-4 py-3 text-xs text-surface-500">
{p.scheduled_date ? new Date(p.scheduled_date).toLocaleDateString() : '—'}
</td>
<td className="px-4 py-3 text-xs text-surface-500">
{p.paid_date ? new Date(p.paid_date).toLocaleDateString() : '—'}
</td>
<td className="px-4 py-3 text-center">
<StatusBadge status={p.status} />
</td>
<td className="px-4 py-3 text-right">
{p.status === 'scheduled' && (
<button
onClick={() => completePayout.mutate(p.id)}
disabled={completePayout.isPending}
className="inline-flex items-center gap-1 rounded bg-green-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-green-700 min-h-[32px]"
>
{completePayout.isPending ? <Loader2 className="h-3 w-3 animate-spin" /> : <CheckCircle2 className="h-3 w-3" />}
Mark Paid
</button>
)}
{p.status !== 'scheduled' && (
<span className="text-xs text-surface-400">—</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{/* ─── Create Partnership Modal ─── */}
{showCreateModal && (
<Modal onClose={() => setShowCreateModal(false)} title="Create Partnership">
<CreatePartnershipForm
onSubmit={(data) => createPartnership.mutate(data)}
loading={createPartnership.isPending}
error={createPartnership.error?.message}
/>
</Modal>
)}
{/* ─── Update Rate Modal ─── */}
{showUpdateRateModal !== null && (
<Modal onClose={() => setShowUpdateRateModal(null)} title="Update Commission Rate">
<form
onSubmit={(e) => {
e.preventDefault();
const rate = parseFloat(newRate) / 100;
if (!isNaN(rate)) {
updateRate.mutate({ id: showUpdateRateModal, rate });
}
}}
className="space-y-4"
>
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">Commission Rate (%)</label>
<div className="relative">
<input
type="number"
step="0.1"
value={newRate}
onChange={(e) => setNewRate(e.target.value)}
className="w-full border border-surface-300 px-3 py-2.5 pr-8 text-sm rounded-lg outline-none focus:border-brand-500 min-h-[44px]"
placeholder="15"
autoFocus
/>
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-surface-400 text-sm">%</span>
</div>
</div>
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => setShowUpdateRateModal(null)}
className="px-4 py-2 text-sm rounded-lg border border-surface-300 hover:bg-surface-50 min-h-[44px]"
>
Cancel
</button>
<button
type="submit"
disabled={updateRate.isPending || !newRate}
className="px-4 py-2 text-sm rounded-lg bg-brand-600 text-white hover:bg-brand-700 disabled:opacity-50 min-h-[44px] flex items-center gap-2"
>
{updateRate.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
Update Rate
</button>
</div>
</form>
</Modal>
)}
{/* ─── Terminate Modal ─── */}
{showTerminateModal !== null && (
<Modal onClose={() => setShowTerminateModal(null)} title="Terminate Partnership">
<div className="space-y-4">
<div className="flex items-start gap-3 rounded-lg bg-red-50 p-4">
<AlertTriangle className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-red-800">Are you sure?</p>
<p className="text-xs text-red-600 mt-1">
This will terminate the partnership agreement. Pending commission records will remain but no new records will be calculated.
</p>
</div>
</div>
<div className="flex justify-end gap-2">
<button
onClick={() => setShowTerminateModal(null)}
className="px-4 py-2 text-sm rounded-lg border border-surface-300 hover:bg-surface-50 min-h-[44px]"
>
Cancel
</button>
<button
onClick={() => terminatePartnership.mutate(showTerminateModal)}
disabled={terminatePartnership.isPending}
className="px-4 py-2 text-sm rounded-lg bg-red-600 text-white hover:bg-red-700 disabled:opacity-50 min-h-[44px] flex items-center gap-2"
>
{terminatePartnership.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
Terminate
</button>
</div>
</div>
</Modal>
)}
{/* ─── Calculate Modal ─── */}
{showCalculateModal && (
<Modal onClose={() => setShowCalculateModal(false)} title="Calculate Commissions">
<CalculateForm
onSubmit={(period) => calculateCommissions.mutate(period)}
loading={calculateCommissions.isPending}
error={calculateCommissions.error?.message}
result={calculateCommissions.data}
/>
</Modal>
)}
{/* ─── Create Payout Modal ─── */}
{showPayoutModal && (
<Modal onClose={() => setShowPayoutModal(false)} title="Create Payout">
<form
onSubmit={(e) => {
e.preventDefault();
if (selectedPartnerId) {
createPayout.mutate({ partner_id: selectedPartnerId });
}
}}
className="space-y-4"
>
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">Partner</label>
<select
value={selectedPartnerId ?? ''}
onChange={(e) => setSelectedPartnerId(parseInt(e.target.value))}
className="w-full border border-surface-300 px-3 py-2.5 text-sm rounded-lg outline-none focus:border-brand-500 min-h-[44px]"
required
>
<option value="">Select a partner...</option>
{partnersList?.map(p => (
<option key={p.partner_id} value={p.partner_id}>
{p.partner_name}
</option>
))}
</select>
</div>
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => { setShowPayoutModal(false); setSelectedPartnerId(null); }}
className="px-4 py-2 text-sm rounded-lg border border-surface-300 hover:bg-surface-50 min-h-[44px]"
>
Cancel
</button>
<button
type="submit"
disabled={createPayout.isPending || !selectedPartnerId}
className="px-4 py-2 text-sm rounded-lg bg-brand-600 text-white hover:bg-brand-700 disabled:opacity-50 min-h-[44px] flex items-center gap-2"
>
{createPayout.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
Create Payout
</button>
</div>
</form>
</Modal>
)}
</AppLayout>
);
}
// ─── Modal Component ────────────────────────────────────────────────────
function Modal({ children, onClose, title }: {
children: React.ReactNode;
onClose: () => void;
title: string;
}) {
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
onClick={onClose}
>
<div
className="w-full max-w-lg rounded-xl bg-white p-6 shadow-xl mx-4"
onClick={(e) => e.stopPropagation()}
>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold text-surface-900">{title}</h2>
<button
onClick={onClose}
className="p-1 rounded-lg hover:bg-surface-100 text-surface-400"
>
<X className="h-5 w-5" />
</button>
</div>
{children}
</div>
</div>
);
}
// ─── Create Partnership Form ────────────────────────────────────────────
function CreatePartnershipForm({
onSubmit,
loading,
error,
}: {
onSubmit: (data: { partner_email: string; company_id: number; commission_rate: number; revenue_source: string }) => void;
loading: boolean;
error?: string;
}) {
const [partnerEmail, setPartnerEmail] = useState('');
const [companyId, setCompanyId] = useState('');
const [commissionRate, setCommissionRate] = useState('15');
const [revenueSource, setRevenueSource] = useState('self_reported');
const { data: companies } = useQuery<{ items: Array<{ id: number; name: string }> }>({
queryKey: ['admin-companies'],
queryFn: () => adminFetch('/api/super-admin/organizations'),
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit({
partner_email: partnerEmail,
company_id: parseInt(companyId),
commission_rate: parseFloat(commissionRate) / 100,
revenue_source: revenueSource,
});
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="rounded-lg bg-red-50 p-3 text-sm text-red-700">{error}</div>
)}
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">Partner Email</label>
<input
type="email"
value={partnerEmail}
onChange={(e) => setPartnerEmail(e.target.value)}
className="w-full border border-surface-300 px-3 py-2.5 text-sm rounded-lg outline-none focus:border-brand-500 min-h-[44px]"
placeholder="partner@example.com"
required
autoFocus
/>
</div>
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">Company</label>
<select
value={companyId}
onChange={(e) => setCompanyId(e.target.value)}
className="w-full border border-surface-300 px-3 py-2.5 text-sm rounded-lg outline-none focus:border-brand-500 min-h-[44px]"
required
>
<option value="">Select a company...</option>
{companies?.items?.map(c => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">Commission Rate (%)</label>
<div className="relative">
<input
type="number"
step="0.1"
value={commissionRate}
onChange={(e) => setCommissionRate(e.target.value)}
className="w-full border border-surface-300 px-3 py-2.5 pr-8 text-sm rounded-lg outline-none focus:border-brand-500 min-h-[44px]"
placeholder="15"
required
/>
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-surface-400 text-sm">%</span>
</div>
</div>
<div>
<label className="block text-sm font-medium text-surface-700 mb-1">Revenue Source</label>
<select
value={revenueSource}
onChange={(e) => setRevenueSource(e.target.value)}
className="w-full border border-surface-300 px-3 py-2.5 text-sm rounded-lg outline-none focus:border-brand-500 min-h-[44px]"
>
<option value="self_reported">Self-reported</option>
<option value="forecast">Forecast-based</option>
<option value="connector">Connector-synced</option>
</select>
</div>
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => {}}
className="px-4 py-2 text-sm rounded-lg border border-surface-300 hover:bg-surface-50 min-h-[44px]"
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className="px-4 py-2 text-sm rounded-lg bg-brand-600 text-white hover:bg-brand-700 disabled:opacity-50 min-h-[44px] flex items-center gap-2"
>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
Create Partnership
</button>
</div>
</form>
);
}
// ─── Calculate Form ─────────────────────────────────────────────────────
function CalculateForm({
onSubmit,
loading,
error,
result,
}: {
onSubmit: (period: string) => void;
loading: boolean;
error?: string;
result?: CalculateResult;
}) {
const now = new Date();
const defaultPeriod = now.getMonth() === 0
? `${now.getFullYear() - 1}-12`
: `${now.getFullYear()}-${String(now.getMonth()).padStart(2, '0')}`;
const [period, setPeriod] = useState(defaultPeriod);
if (result) {
return (
<div className="space-y-4">
<div className="rounded-lg bg-green-50 p-4 text-sm text-green-700">
<div className="flex items-center gap-2 font-medium">
<CheckCircle2 className="h-5 w-5" />
Calculation Complete
</div>
<div className="mt-2 space-y-1">
<p>Period: <strong>{result.period}</strong></p>
<p>Records created: <strong>{result.created}</strong></p>
<p>Records updated: <strong>{result.updated}</strong></p>
<p>Partnerships processed: <strong>{result.partnerships_processed}</strong></p>
</div>
</div>
<div className="flex justify-end">
<button
onClick={() => onSubmit('')}
className="px-4 py-2 text-sm rounded-lg bg-surface-900 text-white hover:bg-surface-800 min-h-[44px]"
>
Done
</button>
</div>
</div>
);
}
return (
<div className="space-y-4">
{error && (
<div className="rounded-lg bg-red-50 p-3 text-sm text-red-700">{error}</div>
)}
<div className="rounded-lg bg-amber-50 p-4">
<div className="flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-amber-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-amber-800">Commission Calculation</p>
<p className="text-xs text-amber-600 mt-1">
This will calculate commissions for all active partnerships based on their configured revenue source.
Revenue pulls from connectors, forecasts, or self-reported values.
</p>
</div>
</div>
</div>
<form
onSubmit={(e) => {
e.preventDefault();
onSubmit(period);
}}
>
<div className="mb-4">
<label className="block text-sm font-medium text-surface-700 mb-1">Period</label>
<div className="relative">
<input
type="month"
value={period}
onChange={(e) => setPeriod(e.target.value)}
className="w-full border border-surface-300 px-3 py-2.5 text-sm rounded-lg outline-none focus:border-brand-500 min-h-[44px]"
required
/>
</div>
<p className="mt-1 text-xs text-surface-500">
Default: previous month ({defaultPeriod})
</p>
</div>
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => onSubmit('')}
className="px-4 py-2 text-sm rounded-lg border border-surface-300 hover:bg-surface-50 min-h-[44px]"
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className="px-4 py-2 text-sm rounded-lg bg-amber-600 text-white hover:bg-amber-700 disabled:opacity-50 min-h-[44px] flex items-center gap-2"
>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Calculator className="h-4 w-4" />}
Calculate
</button>
</div>
</form>
</div>
);
}