remove Payments tab and adds sections into budges- costs
This commit is contained in:
@@ -2014,7 +2014,6 @@ const MOCK_PROJECTS = [
|
||||
commissionType: 'percent_gross',
|
||||
commissionRate: 10,
|
||||
commission: 4500,
|
||||
additionalExpenses: 1200,
|
||||
startDate: '2026-01-20',
|
||||
endDate: '2026-03-05',
|
||||
completionPercentage: 55,
|
||||
@@ -2091,7 +2090,6 @@ const MOCK_PROJECTS = [
|
||||
commissionType: 'flat',
|
||||
commissionRate: 850,
|
||||
commission: 850,
|
||||
additionalExpenses: 350,
|
||||
startDate: '2025-11-15',
|
||||
endDate: '2025-12-10',
|
||||
completionPercentage: 100,
|
||||
@@ -2141,7 +2139,6 @@ const MOCK_PROJECTS = [
|
||||
commissionType: 'percent_net',
|
||||
commissionRate: 15,
|
||||
commission: 3200,
|
||||
additionalExpenses: 900,
|
||||
startDate: '2026-01-06',
|
||||
endDate: '2026-02-28',
|
||||
completionPercentage: 42,
|
||||
@@ -2217,7 +2214,6 @@ const MOCK_PROJECTS = [
|
||||
commissionType: 'custom',
|
||||
commissionRate: 5500,
|
||||
commission: 5500,
|
||||
additionalExpenses: 3800,
|
||||
startDate: '2025-12-01',
|
||||
endDate: '2026-02-28',
|
||||
completionPercentage: 78,
|
||||
@@ -2304,7 +2300,6 @@ const MOCK_PROJECTS = [
|
||||
commissionType: 'percent_gross',
|
||||
commissionRate: 10,
|
||||
commission: 2800,
|
||||
additionalExpenses: 600,
|
||||
startDate: '2026-01-06',
|
||||
endDate: '2026-02-20',
|
||||
completionPercentage: 38,
|
||||
@@ -2378,7 +2373,6 @@ const MOCK_PROJECTS = [
|
||||
commissionType: 'flat',
|
||||
commissionRate: 1850,
|
||||
commission: 1850,
|
||||
additionalExpenses: 450,
|
||||
startDate: '2026-02-03',
|
||||
endDate: '2026-03-15',
|
||||
completionPercentage: 22,
|
||||
@@ -2431,7 +2425,6 @@ const MOCK_PROJECTS = [
|
||||
commissionType: 'percent_gross',
|
||||
commissionRate: 10,
|
||||
commission: 12500,
|
||||
additionalExpenses: 2800,
|
||||
startDate: '2026-01-27',
|
||||
endDate: '2026-04-15',
|
||||
completionPercentage: 32,
|
||||
@@ -2526,7 +2519,6 @@ const MOCK_PROJECTS = [
|
||||
commissionType: 'percent_net',
|
||||
commissionRate: 12,
|
||||
commission: 7500,
|
||||
additionalExpenses: 4200,
|
||||
startDate: '2025-11-18',
|
||||
endDate: '2026-02-28',
|
||||
completionPercentage: 88,
|
||||
@@ -2577,7 +2569,6 @@ const MOCK_PROJECTS = [
|
||||
commissionType: 'percent_gross',
|
||||
commissionRate: 10,
|
||||
commission: 4200,
|
||||
additionalExpenses: 1100,
|
||||
startDate: '2026-01-13',
|
||||
endDate: '2026-03-10',
|
||||
completionPercentage: 50,
|
||||
@@ -2626,7 +2617,6 @@ const MOCK_PROJECTS = [
|
||||
commissionType: 'custom',
|
||||
commissionRate: 3500,
|
||||
commission: 3500,
|
||||
additionalExpenses: 800,
|
||||
startDate: '2025-09-15',
|
||||
endDate: '2025-10-28',
|
||||
completionPercentage: 100,
|
||||
@@ -2675,7 +2665,6 @@ const MOCK_PROJECTS = [
|
||||
commissionType: 'flat',
|
||||
commissionRate: 9500,
|
||||
commission: 9500,
|
||||
additionalExpenses: 5200,
|
||||
startDate: '2025-12-08',
|
||||
endDate: '2026-03-20',
|
||||
completionPercentage: 62,
|
||||
@@ -2732,7 +2721,6 @@ const MOCK_PROJECTS = [
|
||||
commissionType: 'percent_gross',
|
||||
commissionRate: 10,
|
||||
commission: 5800,
|
||||
additionalExpenses: 1500,
|
||||
startDate: '2026-01-13',
|
||||
endDate: '2026-04-01',
|
||||
completionPercentage: 28,
|
||||
|
||||
@@ -68,7 +68,6 @@ const tabs = [
|
||||
{ id: 'rfis', label: 'RFIs', icon: FileText },
|
||||
{ id: 'milestones', label: 'Milestones', icon: Milestone },
|
||||
{ id: 'invoices', label: 'Invoices', icon: DollarSign },
|
||||
{ id: 'payments', label: 'Payments', icon: Banknote },
|
||||
{ id: 'risks', label: 'Risk & Issues', icon: AlertCircle },
|
||||
{ id: 'activity', label: 'Activity', icon: Clock },
|
||||
{ id: 'docs', label: 'Docs', icon: FolderOpen },
|
||||
@@ -265,9 +264,8 @@ const OwnerProjectDetail = () => {
|
||||
const activeCommissionConfig = resolvedCommission;
|
||||
|
||||
const profitMetrics = useMemo(() => {
|
||||
if (!project) return { totalCosts: 0, commission: 0, commissionFormula: '', additionalExp: 0, grossProfit: 0, netProfit: 0, grossMargin: 0, netMargin: 0 };
|
||||
if (!project) return { totalCosts: 0, commission: 0, commissionFormula: '', grossProfit: 0, netProfit: 0, grossMargin: 0, netMargin: 0 };
|
||||
const totalCosts = Number(project.actualCost ?? project.spent) || 0;
|
||||
const additionalExp = Number(project.additionalExpenses) || 0;
|
||||
const gross = totalReceived - totalCosts;
|
||||
|
||||
const { type, rate } = activeCommissionConfig;
|
||||
@@ -297,10 +295,10 @@ const OwnerProjectDetail = () => {
|
||||
}
|
||||
commission = Number.isFinite(commission) ? commission : 0;
|
||||
|
||||
const net = gross - commission - additionalExp;
|
||||
const net = gross - commission;
|
||||
const grossMargin = totalReceived > 0 ? (gross / totalReceived) * 100 : 0;
|
||||
const netMargin = totalReceived > 0 ? (net / totalReceived) * 100 : 0;
|
||||
return { totalCosts, commission, commissionFormula, additionalExp, grossProfit: gross, netProfit: net, grossMargin, netMargin };
|
||||
return { totalCosts, commission, commissionFormula, grossProfit: gross, netProfit: net, grossMargin, netMargin };
|
||||
}, [project, totalReceived, activeCommissionConfig]);
|
||||
|
||||
if (!project) {
|
||||
@@ -736,6 +734,23 @@ const OwnerProjectDetail = () => {
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot className="border-t-2 border-zinc-300 dark:border-white/10">
|
||||
<tr className="bg-zinc-50 dark:bg-white/[0.03]">
|
||||
<td className="px-5 py-4" colSpan={2}>
|
||||
<span className="text-xs font-bold uppercase tracking-widest text-zinc-500 dark:text-zinc-400">Totals</span>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-right font-mono text-sm font-bold text-green-600 dark:text-[#39ff14]">{formatCurrency(totalReceived)}</td>
|
||||
<td className="px-5 py-4 text-right font-mono text-sm font-bold text-[#00f0ff]">{formatCurrency(profitMetrics.totalCosts)}</td>
|
||||
{(() => {
|
||||
const totalVariance = profitMetrics.totalCosts - totalReceived;
|
||||
return (
|
||||
<td className={`px-5 py-4 text-right font-mono text-sm font-bold ${totalVariance > 0 ? 'text-[#ff4500]' : 'text-[#39ff14]'}`}>
|
||||
{totalVariance > 0 ? '+' : ''}{formatCurrency(totalVariance)}
|
||||
</td>
|
||||
);
|
||||
})()}
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</NeoCard>
|
||||
@@ -747,6 +762,329 @@ const OwnerProjectDetail = () => {
|
||||
<p className="text-sm mt-2 text-zinc-400 font-mono">Budget: {formatCurrency(project.approvedBudget || project.budget)} · Spent: {formatCurrency(project.actualCost || project.spent)}</p>
|
||||
</NeoCard>
|
||||
)}
|
||||
|
||||
{/* Commission Configuration Card */}
|
||||
<NeoCard spotlightColor="rgba(253, 169, 19, 0.1)" innerClassName="!p-5">
|
||||
<div className="flex items-start justify-between mb-4" onClick={() => openTooltip && setOpenTooltip(null)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign size={18} className={NEON_GOLD} />
|
||||
<h4 className="text-sm font-mono font-bold text-zinc-900 dark:text-white uppercase tracking-widest">Commission</h4>
|
||||
{/* Source Badge */}
|
||||
{(() => {
|
||||
const src = resolvedCommission.source;
|
||||
const badgeCfg = {
|
||||
org: { icon: Building2, label: 'Org Default', color: 'text-sky-500 dark:text-[#00f0ff]', bg: 'bg-sky-50 dark:bg-[#00f0ff]/10', border: 'border-sky-200 dark:border-[#00f0ff]/20' },
|
||||
user: { icon: UserIcon, label: 'User Override', color: 'text-purple-500 dark:text-purple-400', bg: 'bg-purple-50 dark:bg-purple-500/10', border: 'border-purple-200 dark:border-purple-500/20' },
|
||||
job: { icon: Briefcase, label: 'Job Override', color: 'text-amber-600 dark:text-[#fda913]', bg: 'bg-amber-50 dark:bg-[#fda913]/10', border: 'border-amber-200 dark:border-[#fda913]/20' },
|
||||
};
|
||||
const b = badgeCfg[src];
|
||||
const BadgeIcon = b.icon;
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-widest border ${b.color} ${b.bg} ${b.border}`}>
|
||||
<BadgeIcon size={10} />
|
||||
{b.label}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); setCommissionSettingsOpen(true); }}
|
||||
className="p-1.5 rounded-lg hover:bg-amber-500/10 transition-colors group"
|
||||
title="Commission Settings"
|
||||
>
|
||||
<Settings size={15} className="text-zinc-400 dark:text-zinc-500 group-hover:text-amber-500 dark:group-hover:text-[#fda913] transition-colors" />
|
||||
</button>
|
||||
<div className="relative">
|
||||
<button type="button" onClick={(e) => { e.stopPropagation(); setOpenTooltip(prev => prev === 'commission' ? null : 'commission'); }} className="p-1 rounded-lg hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors">
|
||||
<Info size={14} className="text-zinc-400 dark:text-zinc-500" />
|
||||
</button>
|
||||
{openTooltip === 'commission' && (
|
||||
<div className="absolute right-0 top-2 z-30 w-72 p-3 rounded-xl bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 shadow-xl text-xs text-zinc-600 dark:text-zinc-300">
|
||||
<p className="font-bold text-zinc-900 dark:text-white mb-2">Commission Hierarchy</p>
|
||||
<div className="space-y-1.5">
|
||||
<p><span className="font-bold text-amber-600 dark:text-amber-400">Job Override:</span> Highest priority — set per job</p>
|
||||
<p><span className="font-bold text-purple-600 dark:text-purple-400">User Override:</span> Per-user commission settings</p>
|
||||
<p><span className="font-bold text-sky-600 dark:text-[#00f0ff]">Org Default:</span> Organization-wide fallback</p>
|
||||
</div>
|
||||
<div className="mt-2 pt-2 border-t border-zinc-100 dark:border-white/5">
|
||||
<p className="text-[10px] text-zinc-500">Priority: Job → User → Org</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-12 gap-4 items-end">
|
||||
<div className="sm:col-span-4">
|
||||
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 block mb-1.5">Type</label>
|
||||
<select
|
||||
value={activeCommissionConfig.type}
|
||||
onChange={e => setCommissionConfig(prev => ({ ...(prev || { type: activeCommissionConfig.type, rate: activeCommissionConfig.rate }), type: e.target.value, rate: '' }))}
|
||||
className="w-full bg-zinc-50 dark:bg-[#18181b] border border-zinc-200 dark:border-white/10 rounded-xl px-4 py-2.5 text-sm text-zinc-900 dark:text-white outline-none focus:border-amber-500 dark:focus:border-amber-500 focus:ring-1 focus:ring-amber-500/30 transition-all"
|
||||
>
|
||||
{COMMISSION_TYPES.map(ct => (
|
||||
<option key={ct.value} value={ct.value}>{ct.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="sm:col-span-4">
|
||||
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 block mb-1.5">
|
||||
{activeCommissionConfig.type === 'percent_gross' || activeCommissionConfig.type === 'percent_net' ? 'Rate (%)' : 'Amount ($)'}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-zinc-400 dark:text-zinc-500 pointer-events-none">
|
||||
{activeCommissionConfig.type === 'percent_gross' || activeCommissionConfig.type === 'percent_net' ? '%' : '$'}
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
min="0"
|
||||
value={activeCommissionConfig.rate}
|
||||
onChange={e => setCommissionConfig(prev => ({ ...(prev || { type: activeCommissionConfig.type, rate: activeCommissionConfig.rate }), type: activeCommissionConfig.type, rate: e.target.value }))}
|
||||
placeholder={activeCommissionConfig.type === 'percent_gross' || activeCommissionConfig.type === 'percent_net' ? 'e.g. 10' : 'e.g. 5000'}
|
||||
className="w-full bg-zinc-50 dark:bg-[#18181b] border border-zinc-200 dark:border-white/10 rounded-xl pl-8 pr-4 py-2.5 text-sm text-zinc-900 dark:text-white placeholder-zinc-400 dark:placeholder-zinc-600 outline-none focus:border-amber-500 dark:focus:border-amber-500 focus:ring-1 focus:ring-amber-500/30 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sm:col-span-4">
|
||||
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 block mb-1.5">Calculated Commission</label>
|
||||
<div className="bg-zinc-100 dark:bg-black/40 border border-zinc-200 dark:border-white/5 rounded-xl px-4 py-2.5 flex items-center justify-between">
|
||||
<span className="text-lg font-black font-mono text-amber-600 dark:text-[#fda913]">{formatCurrency(profitMetrics.commission)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 px-3 py-2 bg-zinc-50 dark:bg-black/30 rounded-lg border border-zinc-100 dark:border-white/5 flex items-center justify-between">
|
||||
<p className="text-[11px] font-mono text-zinc-500 dark:text-zinc-400">
|
||||
<span className="text-amber-600 dark:text-amber-500 font-bold">Formula: </span>
|
||||
{profitMetrics.commissionFormula}
|
||||
</p>
|
||||
</div>
|
||||
</NeoCard>
|
||||
|
||||
{/* Profit Summary Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<NeoCard
|
||||
spotlightColor={profitMetrics.grossProfit >= 0 ? "rgba(57, 255, 20, 0.12)" : "rgba(255, 0, 60, 0.12)"}
|
||||
innerClassName="!p-5"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp size={18} className={profitMetrics.grossProfit >= 0 ? NEON_GREEN : NEON_RED} />
|
||||
<h4 className="text-xs font-mono font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-widest">Gross Profit</h4>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<button type="button" onClick={(e) => { e.stopPropagation(); setOpenTooltip(prev => prev === 'gross' ? null : 'gross'); }} className="p-1 rounded-lg hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors">
|
||||
<Info size={14} className="text-zinc-400 dark:text-zinc-500" />
|
||||
</button>
|
||||
{openTooltip === 'gross' && (
|
||||
<div className="absolute right-0 top-2 z-30 w-64 p-3 rounded-xl bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 shadow-xl text-xs text-zinc-600 dark:text-zinc-300">
|
||||
<p className="font-bold text-zinc-900 dark:text-white mb-1">Gross Profit Formula</p>
|
||||
<p className="font-mono text-[11px]">Total Received − Total Actual Costs</p>
|
||||
<div className="mt-2 pt-2 border-t border-zinc-100 dark:border-white/5 space-y-1">
|
||||
<p>{formatCurrency(totalReceived)} − {formatCurrency(profitMetrics.totalCosts)} = {formatCurrency(profitMetrics.grossProfit)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`text-3xl sm:text-4xl font-black font-mono tracking-tight ${profitMetrics.grossProfit >= 0 ? NEON_GREEN : NEON_RED}`}>
|
||||
{profitMetrics.grossProfit >= 0 ? '+' : ''}{formatCurrency(profitMetrics.grossProfit)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${
|
||||
profitMetrics.grossMargin >= 0
|
||||
? 'text-green-600 dark:text-[#39ff14] bg-green-50 dark:bg-[#39ff14]/10 border border-green-200 dark:border-[#39ff14]/20'
|
||||
: 'text-red-600 dark:text-[#ff003c] bg-red-50 dark:bg-[#ff003c]/10 border border-red-200 dark:border-[#ff003c]/20'
|
||||
}`}>
|
||||
{profitMetrics.grossMargin >= 0 ? '+' : ''}{profitMetrics.grossMargin.toFixed(1)}% margin
|
||||
</span>
|
||||
</div>
|
||||
</NeoCard>
|
||||
|
||||
<NeoCard
|
||||
spotlightColor={profitMetrics.netProfit >= 0 ? "rgba(57, 255, 20, 0.12)" : "rgba(255, 0, 60, 0.12)"}
|
||||
innerClassName="!p-5"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign size={18} className={profitMetrics.netProfit >= 0 ? NEON_GREEN : NEON_RED} />
|
||||
<h4 className="text-xs font-mono font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-widest">Net Profit</h4>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<button type="button" onClick={(e) => { e.stopPropagation(); setOpenTooltip(prev => prev === 'net' ? null : 'net'); }} className="p-1 rounded-lg hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors">
|
||||
<Info size={14} className="text-zinc-400 dark:text-zinc-500" />
|
||||
</button>
|
||||
{openTooltip === 'net' && (
|
||||
<div className="absolute right-0 top-2 z-30 w-72 p-3 rounded-xl bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 shadow-xl text-xs text-zinc-600 dark:text-zinc-300">
|
||||
<p className="font-bold text-zinc-900 dark:text-white mb-1">Net Profit Formula</p>
|
||||
<p className="font-mono text-[11px]">Gross Profit − Commission</p>
|
||||
<div className="mt-2 pt-2 border-t border-zinc-100 dark:border-white/5 space-y-1">
|
||||
<p>{formatCurrency(profitMetrics.grossProfit)} − {formatCurrency(profitMetrics.commission)} = {formatCurrency(profitMetrics.netProfit)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`text-3xl sm:text-4xl font-black font-mono tracking-tight ${profitMetrics.netProfit >= 0 ? NEON_GREEN : NEON_RED}`}>
|
||||
{profitMetrics.netProfit >= 0 ? '+' : ''}{formatCurrency(profitMetrics.netProfit)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${
|
||||
profitMetrics.netMargin >= 0
|
||||
? 'text-green-600 dark:text-[#39ff14] bg-green-50 dark:bg-[#39ff14]/10 border border-green-200 dark:border-[#39ff14]/20'
|
||||
: 'text-red-600 dark:text-[#ff003c] bg-red-50 dark:bg-[#ff003c]/10 border border-red-200 dark:border-[#ff003c]/20'
|
||||
}`}>
|
||||
{profitMetrics.netMargin >= 0 ? '+' : ''}{profitMetrics.netMargin.toFixed(1)}% margin
|
||||
</span>
|
||||
</div>
|
||||
</NeoCard>
|
||||
</div>
|
||||
|
||||
{/* Payments Received Table */}
|
||||
<NeoCard spotlightColor="rgba(57, 255, 20, 0.1)" innerClassName="p-0 lg:p-0">
|
||||
<div className="p-5 border-b border-zinc-200 dark:border-white/5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Banknote className={NEON_GREEN} size={20} />
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white tracking-widest uppercase">Payments Received</h3>
|
||||
<span className="text-xs font-bold bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/5 px-2 py-0.5 rounded-full text-zinc-500 dark:text-zinc-400">
|
||||
{payments.length}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCreateModalConfig({
|
||||
title: 'Record Payment', icon: Banknote, iconColor: 'text-[#39ff14]', iconBg: 'bg-[#39ff14]/10',
|
||||
submitLabel: 'Save Payment', submitColor: 'bg-[#39ff14]/20 text-[#39ff14] border-[#39ff14]/30',
|
||||
fields: [
|
||||
{ name: 'from', label: 'From / Payer Name', type: 'text', required: true, placeholder: 'e.g. State Farm Insurance' },
|
||||
{ name: 'method', label: 'Payment Method', type: 'select', options: [{ label: 'Cash', value: 'Cash' }, { label: 'Check', value: 'Check' }, { label: 'ACH', value: 'ACH' }, { label: 'Credit Card', value: 'Credit Card' }, { label: 'Wire Transfer', value: 'Wire Transfer' }], required: true },
|
||||
{ name: 'amount', label: 'Amount ($)', type: 'number', required: true },
|
||||
{ name: 'date', label: 'Payment Date', type: 'date', required: true },
|
||||
{ name: 'refNumber', label: 'Check No. / Reference', type: 'text', placeholder: 'e.g. CHK-1234' },
|
||||
{ name: 'memo', label: 'Memo / Notes', type: 'textarea' },
|
||||
],
|
||||
onSubmit: handleAddPayment
|
||||
})}
|
||||
className="bg-[#39ff14]/10 hover:bg-[#39ff14]/20 text-green-600 dark:text-[#39ff14] border border-[#39ff14]/30 px-4 py-2 rounded-lg text-xs font-bold transition flex items-center gap-2"
|
||||
>
|
||||
<Plus size={14} /> Record Payment
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{payments.length > 0 ? (
|
||||
<>
|
||||
<div className="overflow-x-auto w-full hidden md:block">
|
||||
<table className="w-full text-left border-collapse min-w-[800px]">
|
||||
<thead className="bg-zinc-100 dark:bg-[#18181b]/80 border-b border-zinc-200 dark:border-white/10">
|
||||
<tr>
|
||||
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">From / Method</th>
|
||||
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 text-right cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors select-none" onClick={() => togglePaymentSort('amount')}>
|
||||
Amount {paymentSort.field === 'amount' ? (paymentSort.dir === 'asc' ? '↑' : '↓') : ''}
|
||||
</th>
|
||||
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors select-none" onClick={() => togglePaymentSort('date')}>
|
||||
Date {paymentSort.field === 'date' ? (paymentSort.dir === 'asc' ? '↑' : '↓') : ''}
|
||||
</th>
|
||||
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">Check No. / Ref</th>
|
||||
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">Memo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-100 dark:divide-white/5">
|
||||
{paginatedPayments.map((pay) => (
|
||||
<tr key={pay.id} className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors">
|
||||
<td className="px-5 py-4">
|
||||
<div className="font-bold text-sm text-zinc-900 dark:text-white">{pay.from}</div>
|
||||
<span className={`inline-block mt-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-widest ${
|
||||
pay.method === 'ACH' ? 'text-sky-600 dark:text-[#00f0ff] bg-sky-50 dark:bg-blue-500/10 border border-sky-200 dark:border-blue-500/20' :
|
||||
pay.method === 'Check' ? 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20' :
|
||||
pay.method === 'Credit Card' ? 'text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-500/10 border border-purple-200 dark:border-purple-500/20' :
|
||||
pay.method === 'Wire Transfer' ? 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20' :
|
||||
'text-zinc-600 dark:text-zinc-400 bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10'
|
||||
}`}>
|
||||
{pay.method}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-right font-mono text-sm font-bold text-green-600 dark:text-[#39ff14]">
|
||||
{formatCurrency(pay.amount)}
|
||||
</td>
|
||||
<td className="px-5 py-4 text-sm text-zinc-500 dark:text-zinc-400 font-mono">{pay.date}</td>
|
||||
<td className="px-5 py-4 font-mono text-xs text-zinc-500 dark:text-zinc-400">{pay.refNumber || '—'}</td>
|
||||
<td className="px-5 py-4 text-xs text-zinc-500 dark:text-zinc-400 max-w-[200px] truncate">{pay.memo || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="md:hidden p-4 space-y-3">
|
||||
{paginatedPayments.map((pay) => (
|
||||
<div key={pay.id} className="bg-zinc-50 dark:bg-white/[0.03] border border-zinc-100 dark:border-white/[0.06] rounded-2xl p-4 space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="font-bold text-sm text-zinc-900 dark:text-white">{pay.from}</p>
|
||||
<span className={`inline-block mt-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-widest ${
|
||||
pay.method === 'ACH' ? 'text-sky-600 dark:text-[#00f0ff] bg-sky-50 dark:bg-blue-500/10 border border-sky-200 dark:border-blue-500/20' :
|
||||
pay.method === 'Check' ? 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20' :
|
||||
pay.method === 'Credit Card' ? 'text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-500/10 border border-purple-200 dark:border-purple-500/20' :
|
||||
pay.method === 'Wire Transfer' ? 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20' :
|
||||
'text-zinc-600 dark:text-zinc-400 bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10'
|
||||
}`}>
|
||||
{pay.method}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-mono text-lg font-black text-green-600 dark:text-[#39ff14]">{formatCurrency(pay.amount)}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>
|
||||
<span className="text-zinc-500 dark:text-zinc-500">Date</span>
|
||||
<p className="font-mono text-zinc-700 dark:text-zinc-300">{pay.date}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-zinc-500 dark:text-zinc-500">Ref #</span>
|
||||
<p className="font-mono text-zinc-700 dark:text-zinc-300">{pay.refNumber || '—'}</p>
|
||||
</div>
|
||||
</div>
|
||||
{pay.memo && (
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 border-t border-zinc-100 dark:border-white/5 pt-2">{pay.memo}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{totalPaymentPages > 1 && (
|
||||
<div className="px-5 py-4 border-t border-zinc-200 dark:border-white/5 flex items-center justify-between">
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 font-mono">
|
||||
{paymentPage * PAYMENTS_PER_PAGE + 1}–{Math.min((paymentPage + 1) * PAYMENTS_PER_PAGE, sortedPayments.length)} of {sortedPayments.length}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setPaymentPage(p => Math.max(0, p - 1))}
|
||||
disabled={paymentPage === 0}
|
||||
className="p-2 rounded-lg bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 text-zinc-500 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<ChevronLeft size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPaymentPage(p => Math.min(totalPaymentPages - 1, p + 1))}
|
||||
disabled={paymentPage >= totalPaymentPages - 1}
|
||||
className="p-2 rounded-lg bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 text-zinc-500 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="p-16 text-center text-zinc-500">
|
||||
<Banknote size={48} className="mx-auto mb-4 text-zinc-400 dark:text-zinc-600" />
|
||||
<p className="font-bold text-lg text-zinc-900 dark:text-white">No payments recorded yet.</p>
|
||||
<p className="text-sm mt-2 text-zinc-400">Record your first payment to start tracking.</p>
|
||||
</div>
|
||||
)}
|
||||
</NeoCard>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -982,367 +1320,6 @@ const OwnerProjectDetail = () => {
|
||||
</NeoCard>
|
||||
)}
|
||||
|
||||
{/* PAYMENTS TAB */}
|
||||
{activeTab === 'payments' && (
|
||||
<div className="space-y-6 max-w-7xl mx-auto" onClick={() => openTooltip && setOpenTooltip(null)}>
|
||||
{/* Financial Summary Row */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
||||
<SpotlightCard className="p-4">
|
||||
<div className={`text-xl font-extrabold ${NEON_GREEN}`}>{formatCurrency(totalReceived)}</div>
|
||||
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Total Received</div>
|
||||
</SpotlightCard>
|
||||
<SpotlightCard className="p-4">
|
||||
<div className="text-xl font-extrabold text-zinc-900 dark:text-white">{formatCurrency(profitMetrics.totalCosts)}</div>
|
||||
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Total Costs</div>
|
||||
</SpotlightCard>
|
||||
<SpotlightCard className="p-4">
|
||||
<div className="text-xl font-extrabold text-zinc-900 dark:text-white">{formatCurrency(profitMetrics.additionalExp)}</div>
|
||||
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Addl. Expenses</div>
|
||||
</SpotlightCard>
|
||||
</div>
|
||||
|
||||
{/* Commission Configuration Card */}
|
||||
<NeoCard spotlightColor="rgba(253, 169, 19, 0.1)" innerClassName="!p-5">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign size={18} className={NEON_GOLD} />
|
||||
<h4 className="text-sm font-mono font-bold text-zinc-900 dark:text-white uppercase tracking-widest">Commission</h4>
|
||||
{/* Source Badge */}
|
||||
{(() => {
|
||||
const src = resolvedCommission.source;
|
||||
const badgeCfg = {
|
||||
org: { icon: Building2, label: 'Org Default', color: 'text-sky-500 dark:text-[#00f0ff]', bg: 'bg-sky-50 dark:bg-[#00f0ff]/10', border: 'border-sky-200 dark:border-[#00f0ff]/20' },
|
||||
user: { icon: UserIcon, label: 'User Override', color: 'text-purple-500 dark:text-purple-400', bg: 'bg-purple-50 dark:bg-purple-500/10', border: 'border-purple-200 dark:border-purple-500/20' },
|
||||
job: { icon: Briefcase, label: 'Job Override', color: 'text-amber-600 dark:text-[#fda913]', bg: 'bg-amber-50 dark:bg-[#fda913]/10', border: 'border-amber-200 dark:border-[#fda913]/20' },
|
||||
};
|
||||
const b = badgeCfg[src];
|
||||
const BadgeIcon = b.icon;
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-widest border ${b.color} ${b.bg} ${b.border}`}>
|
||||
<BadgeIcon size={10} />
|
||||
{b.label}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); setCommissionSettingsOpen(true); }}
|
||||
className="p-1.5 rounded-lg hover:bg-amber-500/10 transition-colors group"
|
||||
title="Commission Settings"
|
||||
>
|
||||
<Settings size={15} className="text-zinc-400 dark:text-zinc-500 group-hover:text-amber-500 dark:group-hover:text-[#fda913] transition-colors" />
|
||||
</button>
|
||||
<div className="relative">
|
||||
<button type="button" onClick={(e) => { e.stopPropagation(); setOpenTooltip(prev => prev === 'commission' ? null : 'commission'); }} className="p-1 rounded-lg hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors">
|
||||
<Info size={14} className="text-zinc-400 dark:text-zinc-500" />
|
||||
</button>
|
||||
{openTooltip === 'commission' && (
|
||||
<div className="absolute right-0 top-2 z-30 w-72 p-3 rounded-xl bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 shadow-xl text-xs text-zinc-600 dark:text-zinc-300">
|
||||
<p className="font-bold text-zinc-900 dark:text-white mb-2">Commission Hierarchy</p>
|
||||
<div className="space-y-1.5">
|
||||
<p><span className="font-bold text-amber-600 dark:text-amber-400">Job Override:</span> Highest priority — set per job</p>
|
||||
<p><span className="font-bold text-purple-600 dark:text-purple-400">User Override:</span> Per-user commission settings</p>
|
||||
<p><span className="font-bold text-sky-600 dark:text-[#00f0ff]">Org Default:</span> Organization-wide fallback</p>
|
||||
</div>
|
||||
<div className="mt-2 pt-2 border-t border-zinc-100 dark:border-white/5">
|
||||
<p className="text-[10px] text-zinc-500">Priority: Job → User → Org</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-12 gap-4 items-end">
|
||||
{/* Type Selector */}
|
||||
<div className="sm:col-span-4">
|
||||
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 block mb-1.5">Type</label>
|
||||
<select
|
||||
value={activeCommissionConfig.type}
|
||||
onChange={e => setCommissionConfig(prev => ({ ...(prev || { type: activeCommissionConfig.type, rate: activeCommissionConfig.rate }), type: e.target.value, rate: '' }))}
|
||||
className="w-full bg-zinc-50 dark:bg-[#18181b] border border-zinc-200 dark:border-white/10 rounded-xl px-4 py-2.5 text-sm text-zinc-900 dark:text-white outline-none focus:border-amber-500 dark:focus:border-amber-500 focus:ring-1 focus:ring-amber-500/30 transition-all"
|
||||
>
|
||||
{COMMISSION_TYPES.map(ct => (
|
||||
<option key={ct.value} value={ct.value}>{ct.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Rate / Value Input */}
|
||||
<div className="sm:col-span-4">
|
||||
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 block mb-1.5">
|
||||
{activeCommissionConfig.type === 'percent_gross' || activeCommissionConfig.type === 'percent_net' ? 'Rate (%)' : 'Amount ($)'}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-zinc-400 dark:text-zinc-500 pointer-events-none">
|
||||
{activeCommissionConfig.type === 'percent_gross' || activeCommissionConfig.type === 'percent_net' ? '%' : '$'}
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
min="0"
|
||||
value={activeCommissionConfig.rate}
|
||||
onChange={e => setCommissionConfig(prev => ({ ...(prev || { type: activeCommissionConfig.type, rate: activeCommissionConfig.rate }), type: activeCommissionConfig.type, rate: e.target.value }))}
|
||||
placeholder={activeCommissionConfig.type === 'percent_gross' || activeCommissionConfig.type === 'percent_net' ? 'e.g. 10' : 'e.g. 5000'}
|
||||
className="w-full bg-zinc-50 dark:bg-[#18181b] border border-zinc-200 dark:border-white/10 rounded-xl pl-8 pr-4 py-2.5 text-sm text-zinc-900 dark:text-white placeholder-zinc-400 dark:placeholder-zinc-600 outline-none focus:border-amber-500 dark:focus:border-amber-500 focus:ring-1 focus:ring-amber-500/30 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calculated Result */}
|
||||
<div className="sm:col-span-4">
|
||||
<label className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 block mb-1.5">Calculated Commission</label>
|
||||
<div className="bg-zinc-100 dark:bg-black/40 border border-zinc-200 dark:border-white/5 rounded-xl px-4 py-2.5 flex items-center justify-between">
|
||||
<span className="text-lg font-black font-mono text-amber-600 dark:text-[#fda913]">{formatCurrency(profitMetrics.commission)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live Formula Preview */}
|
||||
<div className="mt-3 px-3 py-2 bg-zinc-50 dark:bg-black/30 rounded-lg border border-zinc-100 dark:border-white/5 flex items-center justify-between">
|
||||
<p className="text-[11px] font-mono text-zinc-500 dark:text-zinc-400">
|
||||
<span className="text-amber-600 dark:text-amber-500 font-bold">Formula: </span>
|
||||
{profitMetrics.commissionFormula}
|
||||
</p>
|
||||
</div>
|
||||
</NeoCard>
|
||||
|
||||
{/* Profit Summary Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Gross Profit */}
|
||||
<NeoCard
|
||||
spotlightColor={profitMetrics.grossProfit >= 0 ? "rgba(57, 255, 20, 0.12)" : "rgba(255, 0, 60, 0.12)"}
|
||||
innerClassName="!p-5"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp size={18} className={profitMetrics.grossProfit >= 0 ? NEON_GREEN : NEON_RED} />
|
||||
<h4 className="text-xs font-mono font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-widest">Gross Profit</h4>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<button type="button" onClick={(e) => { e.stopPropagation(); setOpenTooltip(prev => prev === 'gross' ? null : 'gross'); }} className="p-1 rounded-lg hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors">
|
||||
<Info size={14} className="text-zinc-400 dark:text-zinc-500" />
|
||||
</button>
|
||||
{openTooltip === 'gross' && (
|
||||
<div className="absolute right-0 top-2 z-30 w-64 p-3 rounded-xl bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 shadow-xl text-xs text-zinc-600 dark:text-zinc-300">
|
||||
<p className="font-bold text-zinc-900 dark:text-white mb-1">Gross Profit Formula</p>
|
||||
<p className="font-mono text-[11px]">Total Received − Total Actual Costs</p>
|
||||
<div className="mt-2 pt-2 border-t border-zinc-100 dark:border-white/5 space-y-1">
|
||||
<p>{formatCurrency(totalReceived)} − {formatCurrency(profitMetrics.totalCosts)} = {formatCurrency(profitMetrics.grossProfit)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`text-3xl sm:text-4xl font-black font-mono tracking-tight ${profitMetrics.grossProfit >= 0 ? NEON_GREEN : NEON_RED}`}>
|
||||
{profitMetrics.grossProfit >= 0 ? '+' : ''}{formatCurrency(profitMetrics.grossProfit)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${
|
||||
profitMetrics.grossMargin >= 0
|
||||
? 'text-green-600 dark:text-[#39ff14] bg-green-50 dark:bg-[#39ff14]/10 border border-green-200 dark:border-[#39ff14]/20'
|
||||
: 'text-red-600 dark:text-[#ff003c] bg-red-50 dark:bg-[#ff003c]/10 border border-red-200 dark:border-[#ff003c]/20'
|
||||
}`}>
|
||||
{profitMetrics.grossMargin >= 0 ? '+' : ''}{profitMetrics.grossMargin.toFixed(1)}% margin
|
||||
</span>
|
||||
</div>
|
||||
</NeoCard>
|
||||
|
||||
{/* Net Profit */}
|
||||
<NeoCard
|
||||
spotlightColor={profitMetrics.netProfit >= 0 ? "rgba(57, 255, 20, 0.12)" : "rgba(255, 0, 60, 0.12)"}
|
||||
innerClassName="!p-5"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign size={18} className={profitMetrics.netProfit >= 0 ? NEON_GREEN : NEON_RED} />
|
||||
<h4 className="text-xs font-mono font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-widest">Net Profit</h4>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<button type="button" onClick={(e) => { e.stopPropagation(); setOpenTooltip(prev => prev === 'net' ? null : 'net'); }} className="p-1 rounded-lg hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors">
|
||||
<Info size={14} className="text-zinc-400 dark:text-zinc-500" />
|
||||
</button>
|
||||
{openTooltip === 'net' && (
|
||||
<div className="absolute right-0 top-2 z-30 w-72 p-3 rounded-xl bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 shadow-xl text-xs text-zinc-600 dark:text-zinc-300">
|
||||
<p className="font-bold text-zinc-900 dark:text-white mb-1">Net Profit Formula</p>
|
||||
<p className="font-mono text-[11px]">Gross Profit − Commission − Addl. Expenses</p>
|
||||
<div className="mt-2 pt-2 border-t border-zinc-100 dark:border-white/5 space-y-1">
|
||||
<p>{formatCurrency(profitMetrics.grossProfit)} − {formatCurrency(profitMetrics.commission)} − {formatCurrency(profitMetrics.additionalExp)} = {formatCurrency(profitMetrics.netProfit)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`text-3xl sm:text-4xl font-black font-mono tracking-tight ${profitMetrics.netProfit >= 0 ? NEON_GREEN : NEON_RED}`}>
|
||||
{profitMetrics.netProfit >= 0 ? '+' : ''}{formatCurrency(profitMetrics.netProfit)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${
|
||||
profitMetrics.netMargin >= 0
|
||||
? 'text-green-600 dark:text-[#39ff14] bg-green-50 dark:bg-[#39ff14]/10 border border-green-200 dark:border-[#39ff14]/20'
|
||||
: 'text-red-600 dark:text-[#ff003c] bg-red-50 dark:bg-[#ff003c]/10 border border-red-200 dark:border-[#ff003c]/20'
|
||||
}`}>
|
||||
{profitMetrics.netMargin >= 0 ? '+' : ''}{profitMetrics.netMargin.toFixed(1)}% margin
|
||||
</span>
|
||||
</div>
|
||||
</NeoCard>
|
||||
</div>
|
||||
|
||||
{/* Payments Table */}
|
||||
<NeoCard spotlightColor="rgba(57, 255, 20, 0.1)" innerClassName="p-0 lg:p-0">
|
||||
<div className="p-5 border-b border-zinc-200 dark:border-white/5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Banknote className={NEON_GREEN} size={20} />
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white tracking-widest uppercase">Payments Received</h3>
|
||||
<span className="text-xs font-bold bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/5 px-2 py-0.5 rounded-full text-zinc-500 dark:text-zinc-400">
|
||||
{payments.length}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCreateModalConfig({
|
||||
title: 'Record Payment', icon: Banknote, iconColor: 'text-[#39ff14]', iconBg: 'bg-[#39ff14]/10',
|
||||
submitLabel: 'Save Payment', submitColor: 'bg-[#39ff14]/20 text-[#39ff14] border-[#39ff14]/30',
|
||||
fields: [
|
||||
{ name: 'from', label: 'From / Payer Name', type: 'text', required: true, placeholder: 'e.g. State Farm Insurance' },
|
||||
{ name: 'method', label: 'Payment Method', type: 'select', options: [{ label: 'Cash', value: 'Cash' }, { label: 'Check', value: 'Check' }, { label: 'ACH', value: 'ACH' }, { label: 'Credit Card', value: 'Credit Card' }, { label: 'Wire Transfer', value: 'Wire Transfer' }], required: true },
|
||||
{ name: 'amount', label: 'Amount ($)', type: 'number', required: true },
|
||||
{ name: 'date', label: 'Payment Date', type: 'date', required: true },
|
||||
{ name: 'refNumber', label: 'Check No. / Reference', type: 'text', placeholder: 'e.g. CHK-1234' },
|
||||
{ name: 'memo', label: 'Memo / Notes', type: 'textarea' },
|
||||
],
|
||||
onSubmit: handleAddPayment
|
||||
})}
|
||||
className="bg-[#39ff14]/10 hover:bg-[#39ff14]/20 text-green-600 dark:text-[#39ff14] border border-[#39ff14]/30 px-4 py-2 rounded-lg text-xs font-bold transition flex items-center gap-2"
|
||||
>
|
||||
<Plus size={14} /> Record Payment
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{payments.length > 0 ? (
|
||||
<>
|
||||
{/* Desktop Table */}
|
||||
<div className="overflow-x-auto w-full hidden md:block">
|
||||
<table className="w-full text-left border-collapse min-w-[800px]">
|
||||
<thead className="bg-zinc-100 dark:bg-[#18181b]/80 border-b border-zinc-200 dark:border-white/10">
|
||||
<tr>
|
||||
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">From / Method</th>
|
||||
<th
|
||||
className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 text-right cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors select-none"
|
||||
onClick={() => togglePaymentSort('amount')}
|
||||
>
|
||||
Amount {paymentSort.field === 'amount' ? (paymentSort.dir === 'asc' ? '↑' : '↓') : ''}
|
||||
</th>
|
||||
<th
|
||||
className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400 cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-200 transition-colors select-none"
|
||||
onClick={() => togglePaymentSort('date')}
|
||||
>
|
||||
Date {paymentSort.field === 'date' ? (paymentSort.dir === 'asc' ? '↑' : '↓') : ''}
|
||||
</th>
|
||||
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">Check No. / Ref</th>
|
||||
<th className="px-5 py-4 text-xs font-bold uppercase tracking-wider text-zinc-500 dark:text-zinc-400">Memo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-100 dark:divide-white/5">
|
||||
{paginatedPayments.map((pay) => (
|
||||
<tr key={pay.id} className="hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors">
|
||||
<td className="px-5 py-4">
|
||||
<div className="font-bold text-sm text-zinc-900 dark:text-white">{pay.from}</div>
|
||||
<span className={`inline-block mt-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-widest ${
|
||||
pay.method === 'ACH' ? 'text-sky-600 dark:text-[#00f0ff] bg-sky-50 dark:bg-blue-500/10 border border-sky-200 dark:border-blue-500/20' :
|
||||
pay.method === 'Check' ? 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20' :
|
||||
pay.method === 'Credit Card' ? 'text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-500/10 border border-purple-200 dark:border-purple-500/20' :
|
||||
pay.method === 'Wire Transfer' ? 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20' :
|
||||
'text-zinc-600 dark:text-zinc-400 bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10'
|
||||
}`}>
|
||||
{pay.method}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-right font-mono text-sm font-bold text-green-600 dark:text-[#39ff14]">
|
||||
{formatCurrency(pay.amount)}
|
||||
</td>
|
||||
<td className="px-5 py-4 text-sm text-zinc-500 dark:text-zinc-400 font-mono">{pay.date}</td>
|
||||
<td className="px-5 py-4 font-mono text-xs text-zinc-500 dark:text-zinc-400">{pay.refNumber || '—'}</td>
|
||||
<td className="px-5 py-4 text-xs text-zinc-500 dark:text-zinc-400 max-w-[200px] truncate">{pay.memo || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile Card View */}
|
||||
<div className="md:hidden p-4 space-y-3">
|
||||
{paginatedPayments.map((pay) => (
|
||||
<div key={pay.id} className="bg-zinc-50 dark:bg-white/[0.03] border border-zinc-100 dark:border-white/[0.06] rounded-2xl p-4 space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="font-bold text-sm text-zinc-900 dark:text-white">{pay.from}</p>
|
||||
<span className={`inline-block mt-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-widest ${
|
||||
pay.method === 'ACH' ? 'text-sky-600 dark:text-[#00f0ff] bg-sky-50 dark:bg-blue-500/10 border border-sky-200 dark:border-blue-500/20' :
|
||||
pay.method === 'Check' ? 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20' :
|
||||
pay.method === 'Credit Card' ? 'text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-500/10 border border-purple-200 dark:border-purple-500/20' :
|
||||
pay.method === 'Wire Transfer' ? 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20' :
|
||||
'text-zinc-600 dark:text-zinc-400 bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10'
|
||||
}`}>
|
||||
{pay.method}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-mono text-lg font-black text-green-600 dark:text-[#39ff14]">{formatCurrency(pay.amount)}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>
|
||||
<span className="text-zinc-500 dark:text-zinc-500">Date</span>
|
||||
<p className="font-mono text-zinc-700 dark:text-zinc-300">{pay.date}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-zinc-500 dark:text-zinc-500">Ref #</span>
|
||||
<p className="font-mono text-zinc-700 dark:text-zinc-300">{pay.refNumber || '—'}</p>
|
||||
</div>
|
||||
</div>
|
||||
{pay.memo && (
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 border-t border-zinc-100 dark:border-white/5 pt-2">{pay.memo}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPaymentPages > 1 && (
|
||||
<div className="px-5 py-4 border-t border-zinc-200 dark:border-white/5 flex items-center justify-between">
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 font-mono">
|
||||
{paymentPage * PAYMENTS_PER_PAGE + 1}–{Math.min((paymentPage + 1) * PAYMENTS_PER_PAGE, sortedPayments.length)} of {sortedPayments.length}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setPaymentPage(p => Math.max(0, p - 1))}
|
||||
disabled={paymentPage === 0}
|
||||
className="p-2 rounded-lg bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 text-zinc-500 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<ChevronLeft size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPaymentPage(p => Math.min(totalPaymentPages - 1, p + 1))}
|
||||
disabled={paymentPage >= totalPaymentPages - 1}
|
||||
className="p-2 rounded-lg bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 text-zinc-500 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="p-16 text-center text-zinc-500">
|
||||
<Banknote size={48} className="mx-auto mb-4 text-zinc-400 dark:text-zinc-600" />
|
||||
<p className="font-bold text-lg text-zinc-900 dark:text-white">No payments recorded yet.</p>
|
||||
<p className="text-sm mt-2 text-zinc-400">Record your first payment to start tracking.</p>
|
||||
</div>
|
||||
)}
|
||||
</NeoCard>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* RISK & ISSUES TAB */}
|
||||
{activeTab === 'risks' && (
|
||||
<div className="space-y-6">
|
||||
|
||||
Reference in New Issue
Block a user