refactor: remove deprecated components and streamline member coverage calculations, enhance budget management with improved payroll handling, and update UI elements for better clarity
This commit is contained in:
parent
983aeca2dc
commit
09d8794d72
42 changed files with 2166 additions and 2974 deletions
416
pages/index.vue
416
pages/index.vue
|
|
@ -113,8 +113,7 @@
|
|||
title="Revenue Concentration Risk"
|
||||
:description="`${topStreamName} = ${topSourcePct}% of total → consider balancing`"
|
||||
:actions="[
|
||||
{ label: 'Plan Mix', click: () => handleAlertNavigation('/mix', 'concentration') },
|
||||
{ label: 'Scenarios', click: () => handleAlertNavigation('/scenarios', 'diversification') }
|
||||
{ label: 'Plan Mix', click: () => handleAlertNavigation('/mix', 'concentration') }
|
||||
]" />
|
||||
|
||||
<!-- Cushion Breach Alert -->
|
||||
|
|
@ -153,7 +152,6 @@
|
|||
:description="deferredAlert.description"
|
||||
:actions="[
|
||||
{ label: 'Review Members', click: () => handleAlertNavigation('/coop-builder', 'members') },
|
||||
{ label: 'Value Session', click: () => handleAlertNavigation('/session', 'distributions') }
|
||||
]" />
|
||||
|
||||
<!-- Success message when no alerts -->
|
||||
|
|
@ -166,224 +164,8 @@
|
|||
</div>
|
||||
</UCard>
|
||||
|
||||
<!-- Scenario Snapshots -->
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">Scenario Snapshots</h3>
|
||||
</template>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- Current Scenario -->
|
||||
<div class="p-4 border border-neutral-200 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="font-medium text-sm">{{ scenarios.current.name }}</h4>
|
||||
<UBadge color="green" variant="subtle" size="xs">{{ scenarios.current.status }}</UBadge>
|
||||
</div>
|
||||
<div class="text-2xl font-bold mb-1" :class="getRunwayColor(scenarios.current.runway)">
|
||||
{{ Math.round(scenarios.current.runway * 10) / 10 }} months
|
||||
</div>
|
||||
<p class="text-xs text-neutral-600">
|
||||
Net: {{ $format.currency(scenarios.current.monthlyNet) }}/mo
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Quit Jobs Scenario -->
|
||||
<div class="p-4 border border-neutral-200 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="font-medium text-sm">{{ scenarios.quitJobs.name }}</h4>
|
||||
<UBadge color="gray" variant="subtle" size="xs">{{ scenarios.quitJobs.status }}</UBadge>
|
||||
</div>
|
||||
<div class="text-2xl font-bold mb-1" :class="getRunwayColor(scenarios.quitJobs.runway)">
|
||||
{{ Math.round(scenarios.quitJobs.runway * 10) / 10 }} months
|
||||
</div>
|
||||
<p class="text-xs text-neutral-600">
|
||||
Net: {{ $format.currency(scenarios.quitJobs.monthlyNet) }}/mo
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Start Production Scenario -->
|
||||
<div class="p-4 border border-neutral-200 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="font-medium text-sm">{{ scenarios.startProduction.name }}</h4>
|
||||
<UBadge color="gray" variant="subtle" size="xs">{{ scenarios.startProduction.status }}</UBadge>
|
||||
</div>
|
||||
<div class="text-2xl font-bold mb-1" :class="getRunwayColor(scenarios.startProduction.runway)">
|
||||
{{ Math.round(scenarios.startProduction.runway * 10) / 10 }} months
|
||||
</div>
|
||||
<p class="text-xs text-neutral-600">
|
||||
Net: {{ $format.currency(scenarios.startProduction.monthlyNet) }}/mo
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<UButton variant="outline" @click="navigateTo('/scenarios')">
|
||||
Compare All Scenarios
|
||||
</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<!-- Next Value Accounting Session -->
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium">Next Value Accounting Session</h3>
|
||||
<UBadge color="blue" variant="subtle">January 2024</UBadge>
|
||||
</div>
|
||||
</template>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 class="font-medium mb-3">Session Preparation</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<UIcon name="i-heroicons-check-circle" class="text-green-500" />
|
||||
<span class="text-sm">Month closed & reviewed</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<UIcon name="i-heroicons-check-circle" class="text-green-500" />
|
||||
<span class="text-sm">Contributions logged</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<UIcon name="i-heroicons-x-circle" class="text-neutral-400" />
|
||||
<span class="text-sm text-neutral-600">Surplus calculated</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<UIcon name="i-heroicons-x-circle" class="text-neutral-400" />
|
||||
<span class="text-sm text-neutral-600"
|
||||
>Member needs reviewed</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<UProgress value="50" :max="100" color="blue" />
|
||||
<p class="text-xs text-neutral-600 mt-1">2 of 4 items complete</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-medium mb-3">Available for Distribution</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600">Surplus</span>
|
||||
<span class="font-medium text-green-600">{{
|
||||
$format.currency(metrics.finances.surplus || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600">Deferred owed</span>
|
||||
<span class="font-medium text-orange-600">{{
|
||||
$format.currency(
|
||||
metrics.finances.deferredLiabilities.totalDeferred
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600">Savings gap</span>
|
||||
<span class="font-medium text-blue-600">{{
|
||||
$format.currency(metrics.finances.savingsGap || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<UButton color="primary" @click="navigateTo('/session')">
|
||||
Start Session
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<!-- Advanced Planning Panel -->
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium">Advanced Planning</h3>
|
||||
<UButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="showAdvanced = !showAdvanced"
|
||||
:icon="showAdvanced ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'"
|
||||
>
|
||||
{{ showAdvanced ? 'Hide' : 'Show' }} Advanced
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-show="showAdvanced" class="space-y-6">
|
||||
<!-- Stress Tests -->
|
||||
<div class="border rounded-lg p-4">
|
||||
<h4 class="font-medium mb-3">Stress Tests</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700 mb-1 block">Revenue Delay (months)</label>
|
||||
<UInput
|
||||
v-model="stressTests.revenueDelay"
|
||||
type="number"
|
||||
min="0"
|
||||
max="6"
|
||||
size="sm"
|
||||
@input="updateStressTest"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700 mb-1 block">Cost Shock (%)</label>
|
||||
<UInput
|
||||
v-model="stressTests.costShockPct"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
size="sm"
|
||||
@input="updateStressTest"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700 mb-1 block">Major Grant Lost</label>
|
||||
<UToggle
|
||||
v-model="stressTests.grantLost"
|
||||
@update:model-value="updateStressTest"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stress Test Results -->
|
||||
<div v-if="hasStressTest" class="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h5 class="font-medium text-yellow-800">Stress Test Results</h5>
|
||||
<p class="text-sm text-yellow-700">
|
||||
Runway under stress: {{ Math.round(stressedRunway * 10) / 10 }} months
|
||||
({{ Math.round((stressedRunway - metrics.runway) * 10) / 10 }} month change)
|
||||
</p>
|
||||
</div>
|
||||
<UButton size="xs" @click="applyStressTest">Apply to Plan</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Policy Sandbox -->
|
||||
<div class="border rounded-lg p-4">
|
||||
<h4 class="font-medium mb-3">Policy Sandbox</h4>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Try different pay relationships without overwriting your current plan.
|
||||
</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700 mb-1 block">Test Pay Policy</label>
|
||||
<USelect
|
||||
v-model="sandboxPolicy"
|
||||
:options="policyOptions"
|
||||
size="sm"
|
||||
@update:model-value="updateSandboxPolicy"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="sandboxRunway">
|
||||
<label class="text-sm font-medium text-gray-700 mb-1 block">Projected Runway</label>
|
||||
<div class="text-lg font-bold" :class="getRunwayColor(sandboxRunway)">
|
||||
{{ Math.round(sandboxRunway * 10) / 10 }} months
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
|
|
@ -398,38 +180,7 @@
|
|||
</div>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
block
|
||||
variant="ghost"
|
||||
class="justify-start h-auto p-4"
|
||||
@click="navigateTo('/cash')">
|
||||
<div class="text-left">
|
||||
<div class="font-medium">Cash Calendar</div>
|
||||
<div class="text-xs text-neutral-500">13-week cash flow</div>
|
||||
</div>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
block
|
||||
variant="ghost"
|
||||
class="justify-start h-auto p-4"
|
||||
@click="navigateTo('/scenarios')">
|
||||
<div class="text-left">
|
||||
<div class="font-medium">Scenarios</div>
|
||||
<div class="text-xs text-neutral-500">What-if analysis</div>
|
||||
</div>
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
block
|
||||
color="primary"
|
||||
class="justify-start h-auto p-4"
|
||||
@click="navigateTo('/session')">
|
||||
<div class="text-left">
|
||||
<div class="font-medium">Next Session</div>
|
||||
<div class="text-xs">Value Accounting</div>
|
||||
</div>
|
||||
</UButton>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
|
@ -451,27 +202,6 @@ const { getDualModeRunway, getMonthlyBurn } = useRunway();
|
|||
// Cushion forecast and savings progress
|
||||
const { savingsProgress, cushionForecast, alerts } = useCushionForecast();
|
||||
|
||||
// Scenario calculations
|
||||
const { scenarios } = useScenarios();
|
||||
|
||||
// Advanced panel state
|
||||
const showAdvanced = ref(false);
|
||||
|
||||
// Stress testing
|
||||
const stressTests = ref({
|
||||
revenueDelay: 0,
|
||||
costShockPct: 0,
|
||||
grantLost: false
|
||||
});
|
||||
|
||||
// Policy sandbox
|
||||
const sandboxPolicy = ref('equal-pay');
|
||||
const policyOptions = [
|
||||
{ label: 'Equal Pay', value: 'equal-pay' },
|
||||
{ label: 'Needs Weighted', value: 'needs-weighted' },
|
||||
{ label: 'Hours Weighted', value: 'hours-weighted' },
|
||||
{ label: 'Role Banded', value: 'role-banded' }
|
||||
];
|
||||
|
||||
// Calculate metrics from real store data
|
||||
const metrics = computed(() => {
|
||||
|
|
@ -573,21 +303,6 @@ function getRunwayColor(months: number): string {
|
|||
return 'text-red-600'
|
||||
}
|
||||
|
||||
// Calculate scenario metrics
|
||||
const scenarioMetrics = computed(() => {
|
||||
const baseRunway = metrics.value.runway;
|
||||
return {
|
||||
current: {
|
||||
runway: Math.round(baseRunway * 100) / 100 || 0,
|
||||
},
|
||||
quitJobs: {
|
||||
runway: Math.round(baseRunway * 0.7 * 100) / 100 || 0, // Shorter runway due to higher costs
|
||||
},
|
||||
startProduction: {
|
||||
runway: Math.round(baseRunway * 0.8 * 100) / 100 || 0, // Moderate impact
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Cash breach description
|
||||
const cashBreachDescription = computed(() => {
|
||||
|
|
@ -629,135 +344,6 @@ const onImport = async () => {
|
|||
|
||||
const { exportAll, importAll } = useFixtureIO();
|
||||
|
||||
// Advanced panel computed properties and methods
|
||||
const hasStressTest = computed(() => {
|
||||
return stressTests.value.revenueDelay > 0 ||
|
||||
stressTests.value.costShockPct > 0 ||
|
||||
stressTests.value.grantLost;
|
||||
});
|
||||
|
||||
const stressedRunway = computed(() => {
|
||||
if (!hasStressTest.value) return metrics.value.runway;
|
||||
|
||||
const cash = cashStore.currentCash || 50000;
|
||||
const savings = cashStore.currentSavings || 15000;
|
||||
|
||||
// Apply stress test adjustments
|
||||
let adjustedRevenue = streamsStore.streams.reduce((sum, s) => sum + (s.targetMonthlyAmount || 0), 0);
|
||||
let adjustedCosts = getMonthlyBurn();
|
||||
|
||||
// Revenue delay impact (reduce revenue by delay percentage)
|
||||
if (stressTests.value.revenueDelay > 0) {
|
||||
adjustedRevenue *= Math.max(0, 1 - (stressTests.value.revenueDelay / 12));
|
||||
}
|
||||
|
||||
// Cost shock impact
|
||||
if (stressTests.value.costShockPct > 0) {
|
||||
adjustedCosts *= (1 + stressTests.value.costShockPct / 100);
|
||||
}
|
||||
|
||||
// Grant lost (remove largest revenue stream if it's a grant)
|
||||
if (stressTests.value.grantLost) {
|
||||
const grantStreams = streamsStore.streams.filter(s =>
|
||||
s.category?.toLowerCase().includes('grant') ||
|
||||
s.name.toLowerCase().includes('grant')
|
||||
);
|
||||
if (grantStreams.length > 0) {
|
||||
const largestGrant = Math.max(...grantStreams.map(s => s.targetMonthlyAmount || 0));
|
||||
adjustedRevenue -= largestGrant;
|
||||
}
|
||||
}
|
||||
|
||||
const netMonthly = adjustedRevenue - adjustedCosts;
|
||||
const burnRate = netMonthly < 0 ? Math.abs(netMonthly) : adjustedCosts;
|
||||
|
||||
return burnRate > 0 ? (cash + savings) / burnRate : Infinity;
|
||||
});
|
||||
|
||||
const sandboxRunway = computed(() => {
|
||||
if (!sandboxPolicy.value || sandboxPolicy.value === policiesStore.payPolicy?.relationship) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate runway with sandbox policy
|
||||
const cash = cashStore.currentCash || 50000;
|
||||
const savings = cashStore.currentSavings || 15000;
|
||||
|
||||
// Create sandbox policy object
|
||||
const testPolicy = {
|
||||
relationship: sandboxPolicy.value,
|
||||
equalHourlyWage: policiesStore.equalHourlyWage,
|
||||
roleBands: policiesStore.payPolicy?.roleBands || []
|
||||
};
|
||||
|
||||
// Use scenario calculation with sandbox policy
|
||||
const { calculateScenarioRunway } = useScenarios();
|
||||
const result = calculateScenarioRunway(membersStore.members, streamsStore.streams);
|
||||
|
||||
// Apply simple adjustment based on policy type
|
||||
let policyMultiplier = 1;
|
||||
switch (sandboxPolicy.value) {
|
||||
case 'needs-weighted':
|
||||
policyMultiplier = 0.9; // Slightly higher costs
|
||||
break;
|
||||
case 'role-banded':
|
||||
policyMultiplier = 0.85; // Higher costs due to senior roles
|
||||
break;
|
||||
case 'hours-weighted':
|
||||
policyMultiplier = 0.95; // Moderate increase
|
||||
break;
|
||||
}
|
||||
|
||||
return result.runway * policyMultiplier;
|
||||
});
|
||||
|
||||
function updateStressTest() {
|
||||
// Reactive computed will handle updates automatically
|
||||
}
|
||||
|
||||
function updateSandboxPolicy() {
|
||||
// Reactive computed will handle updates automatically
|
||||
}
|
||||
|
||||
function applyStressTest() {
|
||||
// Apply stress test adjustments to the actual plan
|
||||
if (stressTests.value.revenueDelay > 0) {
|
||||
// Reduce all stream targets by delay impact
|
||||
streamsStore.streams.forEach(stream => {
|
||||
const reduction = (stressTests.value.revenueDelay / 12) * (stream.targetMonthlyAmount || 0);
|
||||
streamsStore.updateStream(stream.id, {
|
||||
targetMonthlyAmount: Math.max(0, (stream.targetMonthlyAmount || 0) - reduction)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (stressTests.value.costShockPct > 0) {
|
||||
// Increase overhead costs
|
||||
const shockMultiplier = 1 + (stressTests.value.costShockPct / 100);
|
||||
budgetStore.overheadCosts.forEach(cost => {
|
||||
budgetStore.updateOverheadCost(cost.id, {
|
||||
amount: (cost.amount || 0) * shockMultiplier
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (stressTests.value.grantLost) {
|
||||
// Remove or reduce grant streams
|
||||
const grantStreams = streamsStore.streams.filter(s =>
|
||||
s.category?.toLowerCase().includes('grant') ||
|
||||
s.name.toLowerCase().includes('grant')
|
||||
);
|
||||
if (grantStreams.length > 0) {
|
||||
const largestGrant = grantStreams.reduce((prev, current) =>
|
||||
(prev.targetMonthlyAmount || 0) > (current.targetMonthlyAmount || 0) ? prev : current
|
||||
);
|
||||
streamsStore.updateStream(largestGrant.id, { targetMonthlyAmount: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// Reset stress tests
|
||||
stressTests.value = { revenueDelay: 0, costShockPct: 0, grantLost: false };
|
||||
};
|
||||
|
||||
// Deferred alert logic
|
||||
const deferredAlert = computed(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue