add commission section
This commit is contained in:
@@ -2011,6 +2011,8 @@ const MOCK_PROJECTS = [
|
|||||||
spent: 22500,
|
spent: 22500,
|
||||||
variancePercent: -50.0,
|
variancePercent: -50.0,
|
||||||
margin: 0.40,
|
margin: 0.40,
|
||||||
|
commissionType: 'percent_gross',
|
||||||
|
commissionRate: 10,
|
||||||
commission: 4500,
|
commission: 4500,
|
||||||
additionalExpenses: 1200,
|
additionalExpenses: 1200,
|
||||||
startDate: '2026-01-20',
|
startDate: '2026-01-20',
|
||||||
@@ -2086,6 +2088,8 @@ const MOCK_PROJECTS = [
|
|||||||
spent: 8500,
|
spent: 8500,
|
||||||
variancePercent: 0,
|
variancePercent: 0,
|
||||||
margin: 0.35,
|
margin: 0.35,
|
||||||
|
commissionType: 'flat',
|
||||||
|
commissionRate: 850,
|
||||||
commission: 850,
|
commission: 850,
|
||||||
additionalExpenses: 350,
|
additionalExpenses: 350,
|
||||||
startDate: '2025-11-15',
|
startDate: '2025-11-15',
|
||||||
@@ -2134,6 +2138,8 @@ const MOCK_PROJECTS = [
|
|||||||
spent: 18400,
|
spent: 18400,
|
||||||
variancePercent: -42.5,
|
variancePercent: -42.5,
|
||||||
margin: 0.30,
|
margin: 0.30,
|
||||||
|
commissionType: 'percent_net',
|
||||||
|
commissionRate: 15,
|
||||||
commission: 3200,
|
commission: 3200,
|
||||||
additionalExpenses: 900,
|
additionalExpenses: 900,
|
||||||
startDate: '2026-01-06',
|
startDate: '2026-01-06',
|
||||||
@@ -2208,6 +2214,8 @@ const MOCK_PROJECTS = [
|
|||||||
spent: 62300,
|
spent: 62300,
|
||||||
variancePercent: 13.3,
|
variancePercent: 13.3,
|
||||||
margin: -0.13,
|
margin: -0.13,
|
||||||
|
commissionType: 'custom',
|
||||||
|
commissionRate: 5500,
|
||||||
commission: 5500,
|
commission: 5500,
|
||||||
additionalExpenses: 3800,
|
additionalExpenses: 3800,
|
||||||
startDate: '2025-12-01',
|
startDate: '2025-12-01',
|
||||||
@@ -2293,6 +2301,8 @@ const MOCK_PROJECTS = [
|
|||||||
spent: 14200,
|
spent: 14200,
|
||||||
variancePercent: -49.3,
|
variancePercent: -49.3,
|
||||||
margin: 0.25,
|
margin: 0.25,
|
||||||
|
commissionType: 'percent_gross',
|
||||||
|
commissionRate: 10,
|
||||||
commission: 2800,
|
commission: 2800,
|
||||||
additionalExpenses: 600,
|
additionalExpenses: 600,
|
||||||
startDate: '2026-01-06',
|
startDate: '2026-01-06',
|
||||||
@@ -2365,6 +2375,8 @@ const MOCK_PROJECTS = [
|
|||||||
spent: 5600,
|
spent: 5600,
|
||||||
variancePercent: -69.7,
|
variancePercent: -69.7,
|
||||||
margin: 0.38,
|
margin: 0.38,
|
||||||
|
commissionType: 'flat',
|
||||||
|
commissionRate: 1850,
|
||||||
commission: 1850,
|
commission: 1850,
|
||||||
additionalExpenses: 450,
|
additionalExpenses: 450,
|
||||||
startDate: '2026-02-03',
|
startDate: '2026-02-03',
|
||||||
@@ -2416,6 +2428,8 @@ const MOCK_PROJECTS = [
|
|||||||
spent: 48000,
|
spent: 48000,
|
||||||
variancePercent: -61.6,
|
variancePercent: -61.6,
|
||||||
margin: 0.35,
|
margin: 0.35,
|
||||||
|
commissionType: 'percent_gross',
|
||||||
|
commissionRate: 10,
|
||||||
commission: 12500,
|
commission: 12500,
|
||||||
additionalExpenses: 2800,
|
additionalExpenses: 2800,
|
||||||
startDate: '2026-01-27',
|
startDate: '2026-01-27',
|
||||||
@@ -2509,6 +2523,8 @@ const MOCK_PROJECTS = [
|
|||||||
spent: 91500,
|
spent: 91500,
|
||||||
variancePercent: 22.0,
|
variancePercent: 22.0,
|
||||||
margin: -0.22,
|
margin: -0.22,
|
||||||
|
commissionType: 'percent_net',
|
||||||
|
commissionRate: 12,
|
||||||
commission: 7500,
|
commission: 7500,
|
||||||
additionalExpenses: 4200,
|
additionalExpenses: 4200,
|
||||||
startDate: '2025-11-18',
|
startDate: '2025-11-18',
|
||||||
@@ -2558,6 +2574,8 @@ const MOCK_PROJECTS = [
|
|||||||
budget: 42000,
|
budget: 42000,
|
||||||
spent: 25800,
|
spent: 25800,
|
||||||
margin: 0.28,
|
margin: 0.28,
|
||||||
|
commissionType: 'percent_gross',
|
||||||
|
commissionRate: 10,
|
||||||
commission: 4200,
|
commission: 4200,
|
||||||
additionalExpenses: 1100,
|
additionalExpenses: 1100,
|
||||||
startDate: '2026-01-13',
|
startDate: '2026-01-13',
|
||||||
@@ -2605,6 +2623,8 @@ const MOCK_PROJECTS = [
|
|||||||
budget: 35000,
|
budget: 35000,
|
||||||
spent: 35000,
|
spent: 35000,
|
||||||
margin: 0.30,
|
margin: 0.30,
|
||||||
|
commissionType: 'custom',
|
||||||
|
commissionRate: 3500,
|
||||||
commission: 3500,
|
commission: 3500,
|
||||||
additionalExpenses: 800,
|
additionalExpenses: 800,
|
||||||
startDate: '2025-09-15',
|
startDate: '2025-09-15',
|
||||||
@@ -2652,6 +2672,8 @@ const MOCK_PROJECTS = [
|
|||||||
budget: 95000,
|
budget: 95000,
|
||||||
spent: 68400,
|
spent: 68400,
|
||||||
margin: 0.20,
|
margin: 0.20,
|
||||||
|
commissionType: 'flat',
|
||||||
|
commissionRate: 9500,
|
||||||
commission: 9500,
|
commission: 9500,
|
||||||
additionalExpenses: 5200,
|
additionalExpenses: 5200,
|
||||||
startDate: '2025-12-08',
|
startDate: '2025-12-08',
|
||||||
@@ -2707,6 +2729,8 @@ const MOCK_PROJECTS = [
|
|||||||
budget: 58000,
|
budget: 58000,
|
||||||
spent: 22600,
|
spent: 22600,
|
||||||
margin: 0.32,
|
margin: 0.32,
|
||||||
|
commissionType: 'percent_gross',
|
||||||
|
commissionRate: 10,
|
||||||
commission: 5800,
|
commission: 5800,
|
||||||
additionalExpenses: 1500,
|
additionalExpenses: 1500,
|
||||||
startDate: '2026-01-13',
|
startDate: '2026-01-13',
|
||||||
|
|||||||
@@ -121,6 +121,16 @@ function seedPaymentsMock(projectId) {
|
|||||||
|
|
||||||
const PAYMENTS_PER_PAGE = 5;
|
const PAYMENTS_PER_PAGE = 5;
|
||||||
|
|
||||||
|
const COMMISSION_TYPES = [
|
||||||
|
{ value: 'flat', label: 'Flat Amount', hint: 'Fixed dollar amount' },
|
||||||
|
{ value: 'percent_gross', label: '% of Gross', hint: 'Percentage of Total Received' },
|
||||||
|
{ value: 'percent_net', label: '% of Net', hint: 'Percentage of Gross Profit' },
|
||||||
|
{ value: 'custom', label: 'Custom / Manual', hint: 'Manually entered value' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const formatCurrency = (amt) =>
|
||||||
|
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(Number(amt) || 0);
|
||||||
|
|
||||||
const OwnerProjectDetail = () => {
|
const OwnerProjectDetail = () => {
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -136,6 +146,11 @@ const OwnerProjectDetail = () => {
|
|||||||
const [paymentSort, setPaymentSort] = useState({ field: 'date', dir: 'desc' });
|
const [paymentSort, setPaymentSort] = useState({ field: 'date', dir: 'desc' });
|
||||||
const [paymentPage, setPaymentPage] = useState(0);
|
const [paymentPage, setPaymentPage] = useState(0);
|
||||||
|
|
||||||
|
const [openTooltip, setOpenTooltip] = useState(null);
|
||||||
|
|
||||||
|
// ── Commission config state ─────────────────────────────────────────────
|
||||||
|
const [commissionConfig, setCommissionConfig] = useState(null);
|
||||||
|
|
||||||
// ── Docs state ────────────────────────────────────────────────────────────
|
// ── Docs state ────────────────────────────────────────────────────────────
|
||||||
const [docs, setDocs] = useState(() => seedDocsMock(projectId));
|
const [docs, setDocs] = useState(() => seedDocsMock(projectId));
|
||||||
const [viewDoc, setViewDoc] = useState(null); // doc object being viewed
|
const [viewDoc, setViewDoc] = useState(null); // doc object being viewed
|
||||||
@@ -220,17 +235,53 @@ const OwnerProjectDetail = () => {
|
|||||||
projects.find(p => p.id === projectId && p.ownerId === user?.id)
|
projects.find(p => p.id === projectId && p.ownerId === user?.id)
|
||||||
, [projects, projectId, user]);
|
, [projects, projectId, user]);
|
||||||
|
|
||||||
|
const activeCommissionConfig = useMemo(() => {
|
||||||
|
if (commissionConfig) return commissionConfig;
|
||||||
|
if (!project) return { type: 'flat', rate: 0 };
|
||||||
|
return {
|
||||||
|
type: project.commissionType || 'flat',
|
||||||
|
rate: Number(project.commissionRate) || 0,
|
||||||
|
};
|
||||||
|
}, [commissionConfig, project]);
|
||||||
|
|
||||||
const profitMetrics = useMemo(() => {
|
const profitMetrics = useMemo(() => {
|
||||||
if (!project) return { totalCosts: 0, commission: 0, additionalExp: 0, grossProfit: 0, netProfit: 0, grossMargin: 0, netMargin: 0 };
|
if (!project) return { totalCosts: 0, commission: 0, commissionFormula: '', additionalExp: 0, grossProfit: 0, netProfit: 0, grossMargin: 0, netMargin: 0 };
|
||||||
const totalCosts = Number(project.actualCost ?? project.spent) || 0;
|
const totalCosts = Number(project.actualCost ?? project.spent) || 0;
|
||||||
const commission = Number(project.commission) || 0;
|
|
||||||
const additionalExp = Number(project.additionalExpenses) || 0;
|
const additionalExp = Number(project.additionalExpenses) || 0;
|
||||||
const gross = totalReceived - totalCosts;
|
const gross = totalReceived - totalCosts;
|
||||||
|
|
||||||
|
const { type, rate } = activeCommissionConfig;
|
||||||
|
const safeRate = Number(rate) || 0;
|
||||||
|
let commission = 0;
|
||||||
|
let commissionFormula = '';
|
||||||
|
switch (type) {
|
||||||
|
case 'flat':
|
||||||
|
commission = safeRate;
|
||||||
|
commissionFormula = `Flat Amount: ${formatCurrency(safeRate)}`;
|
||||||
|
break;
|
||||||
|
case 'percent_gross':
|
||||||
|
commission = totalReceived * (safeRate / 100);
|
||||||
|
commissionFormula = `${safeRate}% of Total Received (${formatCurrency(totalReceived)})`;
|
||||||
|
break;
|
||||||
|
case 'percent_net':
|
||||||
|
commission = gross * (safeRate / 100);
|
||||||
|
commissionFormula = `${safeRate}% of Gross Profit (${formatCurrency(gross)})`;
|
||||||
|
break;
|
||||||
|
case 'custom':
|
||||||
|
commission = safeRate;
|
||||||
|
commissionFormula = `Manual Entry: ${formatCurrency(safeRate)}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
commission = safeRate;
|
||||||
|
commissionFormula = `${formatCurrency(safeRate)}`;
|
||||||
|
}
|
||||||
|
commission = Number.isFinite(commission) ? commission : 0;
|
||||||
|
|
||||||
const net = gross - commission - additionalExp;
|
const net = gross - commission - additionalExp;
|
||||||
const grossMargin = totalReceived > 0 ? (gross / totalReceived) * 100 : 0;
|
const grossMargin = totalReceived > 0 ? (gross / totalReceived) * 100 : 0;
|
||||||
const netMargin = totalReceived > 0 ? (net / totalReceived) * 100 : 0;
|
const netMargin = totalReceived > 0 ? (net / totalReceived) * 100 : 0;
|
||||||
return { totalCosts, commission, additionalExp, grossProfit: gross, netProfit: net, grossMargin, netMargin };
|
return { totalCosts, commission, commissionFormula, additionalExp, grossProfit: gross, netProfit: net, grossMargin, netMargin };
|
||||||
}, [project, totalReceived]);
|
}, [project, totalReceived, activeCommissionConfig]);
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
return (
|
return (
|
||||||
@@ -252,9 +303,6 @@ const OwnerProjectDetail = () => {
|
|||||||
|
|
||||||
const contractor = vendors.find(v => v.id === project.contractorId) || null;
|
const contractor = vendors.find(v => v.id === project.contractorId) || null;
|
||||||
|
|
||||||
const formatCurrency = (amt) =>
|
|
||||||
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(amt);
|
|
||||||
|
|
||||||
const getHealthColor = (s) => s >= 70 ? 'text-emerald-500' : s >= 40 ? 'text-amber-500' : 'text-red-500';
|
const getHealthColor = (s) => s >= 70 ? 'text-emerald-500' : s >= 40 ? 'text-amber-500' : 'text-red-500';
|
||||||
|
|
||||||
const getMilestoneColor = (status) => {
|
const getMilestoneColor = (status) => {
|
||||||
@@ -916,9 +964,9 @@ const OwnerProjectDetail = () => {
|
|||||||
|
|
||||||
{/* PAYMENTS TAB */}
|
{/* PAYMENTS TAB */}
|
||||||
{activeTab === 'payments' && (
|
{activeTab === 'payments' && (
|
||||||
<div className="space-y-6 max-w-7xl mx-auto">
|
<div className="space-y-6 max-w-7xl mx-auto" onClick={() => openTooltip && setOpenTooltip(null)}>
|
||||||
{/* Financial Summary Row */}
|
{/* Financial Summary Row */}
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
||||||
<SpotlightCard className="p-4">
|
<SpotlightCard className="p-4">
|
||||||
<div className={`text-xl font-extrabold ${NEON_GREEN}`}>{formatCurrency(totalReceived)}</div>
|
<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>
|
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Total Received</div>
|
||||||
@@ -927,16 +975,91 @@ const OwnerProjectDetail = () => {
|
|||||||
<div className="text-xl font-extrabold text-zinc-900 dark:text-white">{formatCurrency(profitMetrics.totalCosts)}</div>
|
<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>
|
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Total Costs</div>
|
||||||
</SpotlightCard>
|
</SpotlightCard>
|
||||||
<SpotlightCard className="p-4">
|
|
||||||
<div className="text-xl font-extrabold text-zinc-900 dark:text-white">{formatCurrency(profitMetrics.commission)}</div>
|
|
||||||
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Commission</div>
|
|
||||||
</SpotlightCard>
|
|
||||||
<SpotlightCard className="p-4">
|
<SpotlightCard className="p-4">
|
||||||
<div className="text-xl font-extrabold text-zinc-900 dark:text-white">{formatCurrency(profitMetrics.additionalExp)}</div>
|
<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>
|
<div className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 mt-1">Addl. Expenses</div>
|
||||||
</SpotlightCard>
|
</SpotlightCard>
|
||||||
</div>
|
</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>
|
||||||
|
</div>
|
||||||
|
<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 Types</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<p><span className="font-bold text-amber-600 dark:text-amber-400">Flat:</span> Fixed dollar amount</p>
|
||||||
|
<p><span className="font-bold text-amber-600 dark:text-amber-400">% of Gross:</span> Total Received x %</p>
|
||||||
|
<p><span className="font-bold text-amber-600 dark:text-amber-400">% of Net:</span> Gross Profit x %</p>
|
||||||
|
<p><span className="font-bold text-amber-600 dark:text-amber-400">Custom:</span> Manual dollar entry</p>
|
||||||
|
</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 || activeCommissionConfig), 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 || activeCommissionConfig), 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">
|
||||||
|
<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 */}
|
{/* Profit Summary Cards */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
{/* Gross Profit */}
|
{/* Gross Profit */}
|
||||||
@@ -949,16 +1072,19 @@ const OwnerProjectDetail = () => {
|
|||||||
<TrendingUp size={18} className={profitMetrics.grossProfit >= 0 ? NEON_GREEN : NEON_RED} />
|
<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>
|
<h4 className="text-xs font-mono font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-widest">Gross Profit</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative group">
|
<div className="relative">
|
||||||
<Info size={14} className="text-zinc-400 dark:text-zinc-500 cursor-help" />
|
<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">
|
||||||
<div className="absolute right-0 top-6 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 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all pointer-events-none">
|
<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-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>
|
<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">
|
<div className="mt-2 pt-2 border-t border-zinc-100 dark:border-white/5 space-y-1">
|
||||||
<p>{formatCurrency(totalReceived)} − {formatCurrency(profitMetrics.totalCosts)}</p>
|
<p>{formatCurrency(totalReceived)} − {formatCurrency(profitMetrics.totalCosts)} = {formatCurrency(profitMetrics.grossProfit)}</p>
|
||||||
<p className="font-bold">= {formatCurrency(profitMetrics.grossProfit)}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`text-3xl sm:text-4xl font-black font-mono tracking-tight ${profitMetrics.grossProfit >= 0 ? NEON_GREEN : NEON_RED}`}>
|
<div className={`text-3xl sm:text-4xl font-black font-mono tracking-tight ${profitMetrics.grossProfit >= 0 ? NEON_GREEN : NEON_RED}`}>
|
||||||
@@ -985,16 +1111,19 @@ const OwnerProjectDetail = () => {
|
|||||||
<DollarSign size={18} className={profitMetrics.netProfit >= 0 ? NEON_GREEN : NEON_RED} />
|
<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>
|
<h4 className="text-xs font-mono font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-widest">Net Profit</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative group">
|
<div className="relative">
|
||||||
<Info size={14} className="text-zinc-400 dark:text-zinc-500 cursor-help" />
|
<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">
|
||||||
<div className="absolute right-0 top-6 z-50 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 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all pointer-events-none">
|
<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-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>
|
<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">
|
<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)}</p>
|
<p>{formatCurrency(profitMetrics.grossProfit)} − {formatCurrency(profitMetrics.commission)} − {formatCurrency(profitMetrics.additionalExp)} = {formatCurrency(profitMetrics.netProfit)}</p>
|
||||||
<p className="font-bold">= {formatCurrency(profitMetrics.netProfit)}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`text-3xl sm:text-4xl font-black font-mono tracking-tight ${profitMetrics.netProfit >= 0 ? NEON_GREEN : NEON_RED}`}>
|
<div className={`text-3xl sm:text-4xl font-black font-mono tracking-tight ${profitMetrics.netProfit >= 0 ? NEON_GREEN : NEON_RED}`}>
|
||||||
|
|||||||
Reference in New Issue
Block a user