export type PayRelationship = | 'equal-pay' | 'needs-weighted' | 'hours-weighted' | 'custom-formula'; export interface Member { id: string displayName: string hoursPerWeek?: number hoursPerMonth?: number capacity?: { minHours?: number targetHours?: number maxHours?: number } // Existing/planned monthlyPayPlanned?: number // Simplified - only minimum needs for needs-weighted allocation minMonthlyNeeds?: number // Compatibility with existing store payRelationship?: string riskBand?: string externalCoveragePct?: number privacyNeeds?: string deferredHours?: number quarterlyDeferredCap?: number // UI-only derivations coveragePct?: number } export interface PayPolicy { relationship: PayRelationship notes?: string equalBase?: number needsWeight?: number hoursRate?: number customFormula?: string } // Simplified coverage calculation export function coverage(minNeeds = 0, planned = 0) { const coveragePct = minNeeds > 0 ? Math.min(200, (planned / minNeeds) * 100) : undefined return { coveragePct } } export function teamCoverageStats(members: Member[]) { const vals = members .map(m => coverage(m.minMonthlyNeeds, m.monthlyPayPlanned).coveragePct) .filter((v): v is number => typeof v === 'number') if (!vals.length) return { under100: 0, median: undefined, range: undefined, gini: undefined } const sorted = [...vals].sort((a, b) => a - b) const median = sorted[Math.floor(sorted.length / 2)] const range = { min: sorted[0], max: sorted[sorted.length - 1] } // quick Gini on coverage (0 = equal, 1 = unequal) const mean = vals.reduce((a, b) => a + b, 0) / vals.length let gini = 0 if (mean > 0) { let diffSum = 0 for (let i = 0; i < vals.length; i++) for (let j = 0; j < vals.length; j++) diffSum += Math.abs(vals[i] - vals[j]) gini = diffSum / (2 * vals.length * vals.length * mean) } const under100 = vals.filter(v => v < 100).length return { under100, median, range, gini } } // Payroll allocation based on policy export function allocatePayroll(members: Member[], policy: PayPolicy, payrollBudget: number): Member[] { const result = JSON.parse(JSON.stringify(members)) // Safe deep clone if (policy.relationship === 'equal-pay') { const each = payrollBudget / result.length result.forEach(m => m.monthlyPayPlanned = Math.max(0, each)) return result } if (policy.relationship === 'needs-weighted') { const weights = result.map(m => m.minMonthlyNeeds ?? 0) const sum = weights.reduce((a, b) => a + b, 0) || 1 result.forEach((m, i) => { const w = weights[i] / sum m.monthlyPayPlanned = Math.max(0, payrollBudget * w) }) return result } // Removed role-banded allocation - no longer supported if (policy.relationship === 'hours-weighted') { const hours = result.map(m => m.hoursPerMonth ?? (m.hoursPerWeek ? m.hoursPerWeek * 4 : 0) ?? (m.capacity?.targetHours ?? 0)) const sum = hours.reduce((a, b) => a + b, 0) || 1 result.forEach((m, i) => m.monthlyPayPlanned = payrollBudget * (hours[i] / sum)) return result } // fallback: equal const each = payrollBudget / result.length result.forEach(m => m.monthlyPayPlanned = Math.max(0, each)) return result } // Monthly payroll calculation for runway and budgets export function monthlyPayroll(members: Member[], mode: 'minimum' | 'target' = 'minimum'): number { return members.reduce((sum, m) => { const planned = m.monthlyPayPlanned ?? 0 // In "minimum" mode cap at min needs to show a lean runway scenario if (mode === 'minimum' && m.minMonthlyNeeds) { return sum + Math.min(planned, m.minMonthlyNeeds) } return sum + planned }, 0) }