refactor: replace Wizard with CoopBuilder in navigation, enhance budget store structure, and streamline template components for improved user experience
This commit is contained in:
parent
eede87a273
commit
f67b138d95
33 changed files with 4970 additions and 2451 deletions
652
pages/budget.vue
652
pages/budget.vue
|
|
@ -1,368 +1,320 @@
|
|||
<template>
|
||||
<section class="py-8 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-semibold">Operating Plan</h2>
|
||||
<USelect
|
||||
v-model="selectedMonth"
|
||||
:options="months"
|
||||
placeholder="Select month" />
|
||||
<h2 class="text-2xl font-semibold">Budget Worksheet</h2>
|
||||
<div class="flex items-center gap-4">
|
||||
<UButton @click="forceReinitialize" variant="outline" size="sm" color="orange">Force Re-init</UButton>
|
||||
<UButton @click="resetWorksheet" variant="outline" size="sm">Reset All</UButton>
|
||||
<UButton @click="exportBudget" variant="outline" size="sm">Export</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cash Waterfall Summary -->
|
||||
<!-- Budget Worksheet Table -->
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">
|
||||
Cash Waterfall - {{ selectedMonth }}
|
||||
</h3>
|
||||
</template>
|
||||
<div
|
||||
class="flex items-center justify-between py-4 border-b border-neutral-200">
|
||||
<div class="flex items-center gap-8">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-blue-600">
|
||||
€{{ budgetMetrics.grossRevenue.toLocaleString() }}
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">Gross Revenue</div>
|
||||
</div>
|
||||
<UIcon name="i-heroicons-arrow-right" class="text-neutral-400" />
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-red-600">
|
||||
-€{{ budgetMetrics.totalFees.toLocaleString() }}
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">Fees</div>
|
||||
</div>
|
||||
<UIcon name="i-heroicons-arrow-right" class="text-neutral-400" />
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-green-600">
|
||||
€{{ budgetMetrics.netRevenue.toLocaleString() }}
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">Net Revenue</div>
|
||||
</div>
|
||||
<UIcon name="i-heroicons-arrow-right" class="text-neutral-400" />
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-blue-600">
|
||||
€{{ Math.round(budgetMetrics.savingsAmount).toLocaleString() }}
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">To Savings</div>
|
||||
</div>
|
||||
<UIcon name="i-heroicons-arrow-right" class="text-neutral-400" />
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-purple-600">
|
||||
€{{ Math.round(budgetMetrics.totalPayroll).toLocaleString() }}
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">Payroll</div>
|
||||
</div>
|
||||
<UIcon name="i-heroicons-arrow-right" class="text-neutral-400" />
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-orange-600">
|
||||
€{{ budgetMetrics.totalOverhead.toLocaleString() }}
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">Overhead</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-lg font-medium">Available for Operations</span>
|
||||
<span class="text-2xl font-bold text-green-600"
|
||||
>€{{
|
||||
Math.round(budgetMetrics.availableForOps).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full border-collapse border border-gray-300 text-sm">
|
||||
<thead>
|
||||
<tr class="bg-gray-50">
|
||||
<th class="border border-gray-300 px-3 py-2 text-left min-w-40 sticky left-0 bg-gray-50 z-10">Category</th>
|
||||
<!-- Monthly columns -->
|
||||
<th v-for="month in monthlyHeaders" :key="month.key" class="border border-gray-300 px-2 py-2 text-center min-w-20">{{ month.label }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Revenue Section -->
|
||||
<tr class="bg-blue-50 font-medium">
|
||||
<td class="border border-gray-300 px-3 py-2 sticky left-0 bg-blue-50 z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Revenue</span>
|
||||
<UButton @click="addRevenueLine" size="xs" variant="soft">+</UButton>
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-gray-300 px-2 py-2" :colspan="monthlyHeaders.length"></td>
|
||||
</tr>
|
||||
|
||||
<!-- Revenue by Category -->
|
||||
<template v-for="(category, categoryName) in budgetStore.groupedRevenue" :key="`revenue-${categoryName}`">
|
||||
<tr v-if="category.length > 0" class="bg-blue-100 font-medium">
|
||||
<td class="border border-gray-300 px-4 py-1 sticky left-0 bg-blue-100 z-10 text-sm text-blue-700">
|
||||
{{ categoryName }} ({{ category.length }} items)
|
||||
</td>
|
||||
<td class="border border-gray-300 px-2 py-1" :colspan="monthlyHeaders.length"></td>
|
||||
</tr>
|
||||
<tr v-for="item in category" :key="item.id">
|
||||
<td class="border border-gray-300 px-3 py-2 sticky left-0 bg-white z-10">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<input
|
||||
v-model="item.name"
|
||||
@blur="saveWorksheet"
|
||||
class="bg-transparent border-none outline-none w-full font-medium"
|
||||
:class="{ 'italic text-gray-500': item.name === 'New Revenue Item' }"
|
||||
/>
|
||||
<UButton @click="removeItem('revenue', item.id)" size="xs" variant="ghost" color="error">×</UButton>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<BudgetCategorySelector
|
||||
v-model="item.subcategory"
|
||||
type="revenue"
|
||||
:main-category="item.mainCategory"
|
||||
placeholder="Subcategory"
|
||||
@update:model-value="saveWorksheet"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Monthly columns -->
|
||||
<td v-for="month in monthlyHeaders" :key="month.key" class="border border-gray-300 px-1 py-1">
|
||||
<input
|
||||
type="number"
|
||||
:value="item.monthlyValues?.[month.key] || 0"
|
||||
@input="updateMonthlyValue('revenue', item.id, month.key, $event.target.value)"
|
||||
class="w-full text-right border-none outline-none bg-transparent"
|
||||
placeholder="0"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Total Revenue Row -->
|
||||
<tr class="bg-blue-100 font-bold">
|
||||
<td class="border border-gray-300 px-3 py-2 sticky left-0 bg-blue-100 z-10">Total Revenue</td>
|
||||
<td v-for="month in monthlyHeaders" :key="month.key" class="border border-gray-300 px-2 py-2 text-right">
|
||||
{{ formatCurrency(budgetStore.monthlyTotals[month.key]?.revenue || 0) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Expenses Section -->
|
||||
<tr class="bg-red-50 font-medium">
|
||||
<td class="border border-gray-300 px-3 py-2 sticky left-0 bg-red-50 z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Expenses</span>
|
||||
<UButton @click="addExpenseLine" size="xs" variant="soft">+</UButton>
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-gray-300 px-2 py-2" :colspan="monthlyHeaders.length"></td>
|
||||
</tr>
|
||||
|
||||
<!-- Expenses by Category -->
|
||||
<template v-for="(category, categoryName) in budgetStore.groupedExpenses" :key="`expense-${categoryName}`">
|
||||
<tr v-if="category.length > 0" class="bg-red-100 font-medium">
|
||||
<td class="border border-gray-300 px-4 py-1 sticky left-0 bg-red-100 z-10 text-sm text-red-700">
|
||||
{{ categoryName }}
|
||||
</td>
|
||||
<td class="border border-gray-300 px-2 py-1" :colspan="monthlyHeaders.length"></td>
|
||||
</tr>
|
||||
<tr v-for="item in category" :key="item.id">
|
||||
<td class="border border-gray-300 px-3 py-2 sticky left-0 bg-white z-10">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<input
|
||||
v-model="item.name"
|
||||
@blur="saveWorksheet"
|
||||
class="bg-transparent border-none outline-none w-full font-medium"
|
||||
:class="{ 'italic text-gray-500': item.name === 'New Expense Item' }"
|
||||
/>
|
||||
<UButton @click="removeItem('expenses', item.id)" size="xs" variant="ghost" color="error">×</UButton>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<BudgetCategorySelector
|
||||
v-model="item.subcategory"
|
||||
type="expenses"
|
||||
:main-category="item.mainCategory"
|
||||
placeholder="Subcategory"
|
||||
@update:model-value="saveWorksheet"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Monthly columns -->
|
||||
<td v-for="month in monthlyHeaders" :key="month.key" class="border border-gray-300 px-1 py-1">
|
||||
<input
|
||||
type="number"
|
||||
:value="item.monthlyValues?.[month.key] || 0"
|
||||
@input="updateMonthlyValue('expenses', item.id, month.key, $event.target.value)"
|
||||
class="w-full text-right border-none outline-none bg-transparent"
|
||||
placeholder="0"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Total Expenses Row -->
|
||||
<tr class="bg-red-100 font-bold">
|
||||
<td class="border border-gray-300 px-3 py-2 sticky left-0 bg-red-100 z-10">Total Expenses</td>
|
||||
<td v-for="month in monthlyHeaders" :key="month.key" class="border border-gray-300 px-2 py-2 text-right">
|
||||
{{ formatCurrency(budgetStore.monthlyTotals[month.key]?.expenses || 0) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Net Income Row -->
|
||||
<tr class="bg-green-100 font-bold text-lg">
|
||||
<td class="border border-gray-300 px-3 py-2 sticky left-0 bg-green-100 z-10">Net Income</td>
|
||||
<td v-for="month in monthlyHeaders" :key="month.key" class="border border-gray-300 px-2 py-2 text-right"
|
||||
:class="getNetIncomeClass(budgetStore.monthlyTotals[month.key]?.net || 0)">
|
||||
{{ formatCurrency(budgetStore.monthlyTotals[month.key]?.net || 0) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<!-- Monthly Revenue Table -->
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">Revenue by Stream</h3>
|
||||
</template>
|
||||
<UTable :rows="revenueStreams" :columns="revenueColumns">
|
||||
<template #name-data="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">{{ row.name }}</span>
|
||||
<RestrictionChip :restriction="row.restrictions" size="xs" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #target-data="{ row }">
|
||||
<span class="font-medium">€{{ row.target.toLocaleString() }}</span>
|
||||
</template>
|
||||
|
||||
<template #committed-data="{ row }">
|
||||
<span class="font-medium text-green-600"
|
||||
>€{{ row.committed.toLocaleString() }}</span
|
||||
>
|
||||
</template>
|
||||
|
||||
<template #actual-data="{ row }">
|
||||
<span
|
||||
class="font-medium"
|
||||
:class="
|
||||
row.actual >= row.committed ? 'text-green-600' : 'text-orange-600'
|
||||
">
|
||||
€{{ row.actual.toLocaleString() }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #variance-data="{ row }">
|
||||
<span :class="row.variance >= 0 ? 'text-green-600' : 'text-red-600'">
|
||||
{{ row.variance >= 0 ? "+" : "" }}€{{
|
||||
row.variance.toLocaleString()
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</UTable>
|
||||
</UCard>
|
||||
|
||||
<!-- Costs Breakdown -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">Costs</h3>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h4 class="font-medium text-sm mb-2">Payroll</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600"
|
||||
>Wages ({{ budgetMetrics.totalHours }}h @ €{{
|
||||
budgetMetrics.hourlyWage
|
||||
}})</span
|
||||
>
|
||||
<span class="font-medium"
|
||||
>€{{
|
||||
Math.round(budgetMetrics.grossWages).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600"
|
||||
>On-costs ({{ budgetMetrics.oncostPct }}%)</span
|
||||
>
|
||||
<span class="font-medium"
|
||||
>€{{
|
||||
Math.round(budgetMetrics.oncosts).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-between text-sm font-medium border-t pt-2">
|
||||
<span>Total Payroll</span>
|
||||
<span
|
||||
>€{{
|
||||
Math.round(budgetMetrics.totalPayroll).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-medium text-sm mb-2">Overhead</h4>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-if="budgetStore.overheadCosts.length === 0"
|
||||
class="text-sm text-neutral-500 italic">
|
||||
No overhead costs added yet
|
||||
</div>
|
||||
<div
|
||||
v-for="cost in budgetStore.overheadCosts"
|
||||
:key="cost.id"
|
||||
class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600">{{ cost.name }}</span>
|
||||
<span class="font-medium"
|
||||
>€{{ (cost.amount || 0).toLocaleString() }}</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-between text-sm font-medium border-t pt-2">
|
||||
<span>Total Overhead</span>
|
||||
<span>€{{ budgetMetrics.totalOverhead.toLocaleString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-medium text-sm mb-2">Production</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600">Dev kits</span>
|
||||
<span class="font-medium">€500</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-between text-sm font-medium border-t pt-2">
|
||||
<span>Total Production</span>
|
||||
<span>€500</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">Net Impact on Savings</h3>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600">Net Revenue</span>
|
||||
<span class="font-medium text-green-600"
|
||||
>€{{ budgetMetrics.netRevenue.toLocaleString() }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600">Total Costs</span>
|
||||
<span class="font-medium text-red-600"
|
||||
>-€{{
|
||||
Math.round(budgetMetrics.totalCosts).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between text-lg font-bold border-t pt-3">
|
||||
<span>Net</span>
|
||||
<span
|
||||
:class="
|
||||
budgetMetrics.monthlyNet >= 0
|
||||
? 'text-green-600'
|
||||
: 'text-red-600'
|
||||
"
|
||||
>{{ budgetMetrics.monthlyNet >= 0 ? "+" : "" }}€{{
|
||||
Math.round(budgetMetrics.monthlyNet).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-neutral-50 rounded-lg p-4">
|
||||
<h4 class="font-medium text-sm mb-3">Allocation</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600">To Savings</span>
|
||||
<span class="font-medium"
|
||||
>€{{
|
||||
Math.round(budgetMetrics.savingsAmount).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600">Available</span>
|
||||
<span class="font-medium"
|
||||
>€{{
|
||||
Math.round(
|
||||
budgetMetrics.availableAfterSavings
|
||||
).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-neutral-600 space-y-1">
|
||||
<p>
|
||||
<RestrictionChip restriction="Restricted" size="xs" /> funds can
|
||||
only be used for approved purposes.
|
||||
</p>
|
||||
<p>
|
||||
<RestrictionChip restriction="General" size="xs" /> funds have no
|
||||
restrictions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Use real store data
|
||||
const membersStore = useMembersStore();
|
||||
const policiesStore = usePoliciesStore();
|
||||
const streamsStore = useStreamsStore();
|
||||
// Import components explicitly
|
||||
import BudgetCategorySelector from '~/components/BudgetCategorySelector.vue';
|
||||
|
||||
// Use budget worksheet store
|
||||
const budgetStore = useBudgetStore();
|
||||
const cashStore = useCashStore();
|
||||
|
||||
const selectedMonth = ref("2024-01");
|
||||
const months = ref([
|
||||
{ label: "January 2024", value: "2024-01" },
|
||||
{ label: "February 2024", value: "2024-02" },
|
||||
{ label: "March 2024", value: "2024-03" },
|
||||
]);
|
||||
|
||||
// Calculate budget values from real data
|
||||
const budgetMetrics = computed(() => {
|
||||
const totalHours = membersStore.capacityTotals.targetHours || 0;
|
||||
const hourlyWage = policiesStore.equalHourlyWage || 0;
|
||||
const oncostPct = policiesStore.payrollOncostPct || 0;
|
||||
|
||||
const grossWages = totalHours * hourlyWage;
|
||||
const oncosts = grossWages * (oncostPct / 100);
|
||||
const totalPayroll = grossWages + oncosts;
|
||||
|
||||
const totalOverhead = budgetStore.overheadCosts.reduce(
|
||||
(sum, cost) => sum + (cost.amount || 0),
|
||||
0
|
||||
);
|
||||
const grossRevenue = streamsStore.totalMonthlyAmount || 0;
|
||||
|
||||
// Calculate fees from streams with platform fees
|
||||
const totalFees = streamsStore.streams.reduce((sum, stream) => {
|
||||
const revenue = stream.targetMonthlyAmount || 0;
|
||||
const platformFee = (stream.platformFeePct || 0) / 100;
|
||||
const revShareFee = (stream.revenueSharePct || 0) / 100;
|
||||
return sum + revenue * platformFee + revenue * revShareFee;
|
||||
}, 0);
|
||||
|
||||
const netRevenue = grossRevenue - totalFees;
|
||||
const totalCosts = totalPayroll + totalOverhead;
|
||||
const monthlyNet = netRevenue - totalCosts;
|
||||
const savingsAmount = Math.max(0, monthlyNet * 0.3); // Save 30% of positive net if possible
|
||||
const availableAfterSavings = Math.max(0, monthlyNet - savingsAmount);
|
||||
const availableForOps = Math.max(
|
||||
0,
|
||||
netRevenue - totalPayroll - totalOverhead - savingsAmount
|
||||
);
|
||||
|
||||
return {
|
||||
grossRevenue,
|
||||
totalFees,
|
||||
netRevenue,
|
||||
totalCosts,
|
||||
monthlyNet,
|
||||
savingsAmount,
|
||||
availableAfterSavings,
|
||||
totalPayroll,
|
||||
grossWages,
|
||||
oncosts,
|
||||
totalOverhead,
|
||||
availableForOps,
|
||||
totalHours,
|
||||
hourlyWage,
|
||||
oncostPct,
|
||||
};
|
||||
// Generate monthly headers for the next 12 months
|
||||
const monthlyHeaders = computed(() => {
|
||||
const headers = [];
|
||||
const today = new Date();
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const date = new Date(today.getFullYear(), today.getMonth() + i, 1);
|
||||
const monthName = date.toLocaleString('default', { month: 'short' });
|
||||
const year = date.getFullYear();
|
||||
|
||||
headers.push({
|
||||
key: `${year}-${String(date.getMonth() + 1).padStart(2, '0')}`,
|
||||
label: `${monthName} ${year}`
|
||||
});
|
||||
}
|
||||
|
||||
return headers;
|
||||
});
|
||||
|
||||
// Convert streams to budget table format
|
||||
const revenueStreams = computed(() =>
|
||||
streamsStore.streams.map((stream) => ({
|
||||
id: stream.id,
|
||||
name: stream.name,
|
||||
target: stream.targetMonthlyAmount || 0,
|
||||
committed: Math.round((stream.targetMonthlyAmount || 0) * 0.8), // 80% committed assumption
|
||||
actual: Math.round((stream.targetMonthlyAmount || 0) * 0.9), // 90% actual assumption
|
||||
variance: Math.round((stream.targetMonthlyAmount || 0) * 0.1), // 10% positive variance
|
||||
restrictions: stream.restrictions || "General",
|
||||
}))
|
||||
);
|
||||
// Initialize from wizard data on first load
|
||||
onMounted(async () => {
|
||||
console.log('Budget page mounted, initializing...');
|
||||
if (!budgetStore.isInitialized) {
|
||||
await budgetStore.initializeFromWizardData();
|
||||
}
|
||||
console.log('Budget worksheet:', budgetStore.budgetWorksheet);
|
||||
console.log('Grouped revenue:', budgetStore.groupedRevenue);
|
||||
console.log('Grouped expenses:', budgetStore.groupedExpenses);
|
||||
});
|
||||
|
||||
const revenueColumns = [
|
||||
{ id: "name", key: "name", label: "Stream" },
|
||||
{ id: "target", key: "target", label: "Target" },
|
||||
{ id: "committed", key: "committed", label: "Committed" },
|
||||
{ id: "actual", key: "actual", label: "Actual" },
|
||||
{ id: "variance", key: "variance", label: "Variance" },
|
||||
];
|
||||
// Budget worksheet functions
|
||||
function updateValue(category: string, itemId: string, year: string, scenario: string, value: string) {
|
||||
budgetStore.updateBudgetValue(category, itemId, year, scenario, value);
|
||||
}
|
||||
|
||||
function updateMonthlyValue(category: string, itemId: string, monthKey: string, value: string) {
|
||||
budgetStore.updateMonthlyValue(category, itemId, monthKey, value);
|
||||
}
|
||||
|
||||
function addRevenueLine() {
|
||||
console.log('Adding revenue line...');
|
||||
budgetStore.addBudgetItem('revenue', 'New Revenue Item');
|
||||
}
|
||||
|
||||
function addExpenseLine() {
|
||||
console.log('Adding expense line...');
|
||||
budgetStore.addBudgetItem('expenses', 'New Expense Item');
|
||||
}
|
||||
|
||||
function removeItem(category: string, itemId: string) {
|
||||
budgetStore.removeBudgetItem(category, itemId);
|
||||
}
|
||||
|
||||
function saveWorksheet() {
|
||||
// Auto-save is handled by the store persistence
|
||||
console.log('Worksheet saved');
|
||||
}
|
||||
|
||||
function resetWorksheet() {
|
||||
if (confirm('Are you sure you want to reset all budget data? This cannot be undone.')) {
|
||||
budgetStore.resetBudgetWorksheet();
|
||||
// Force re-initialization
|
||||
budgetStore.isInitialized = false;
|
||||
budgetStore.initializeFromWizardData();
|
||||
}
|
||||
}
|
||||
|
||||
async function forceReinitialize() {
|
||||
console.log('Force re-initializing budget...');
|
||||
// Clear all persistent data
|
||||
localStorage.removeItem('urgent-tools-budget');
|
||||
localStorage.removeItem('urgent-tools-streams');
|
||||
localStorage.removeItem('urgent-tools-members');
|
||||
localStorage.removeItem('urgent-tools-policies');
|
||||
|
||||
// Reset the store state completely
|
||||
budgetStore.isInitialized = false;
|
||||
budgetStore.budgetWorksheet.revenue = [];
|
||||
budgetStore.budgetWorksheet.expenses = [];
|
||||
|
||||
// Reset categories to defaults
|
||||
budgetStore.revenueCategories = [
|
||||
'Games & Products',
|
||||
'Services & Contracts',
|
||||
'Grants & Funding',
|
||||
'Community Support',
|
||||
'Partnerships',
|
||||
'Investment Income',
|
||||
'In-Kind Contributions'
|
||||
];
|
||||
|
||||
budgetStore.expenseCategories = [
|
||||
'Salaries & Benefits',
|
||||
'Development Costs',
|
||||
'Equipment & Technology',
|
||||
'Marketing & Outreach',
|
||||
'Office & Operations',
|
||||
'Legal & Professional',
|
||||
'Other Expenses'
|
||||
];
|
||||
|
||||
// Force re-initialization
|
||||
await budgetStore.initializeFromWizardData();
|
||||
|
||||
console.log('Re-initialization complete');
|
||||
}
|
||||
|
||||
function exportBudget() {
|
||||
const data = {
|
||||
worksheet: budgetStore.budgetWorksheet,
|
||||
totals: budgetStore.budgetTotals,
|
||||
exportedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `budget-worksheet-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function formatCurrency(amount: number): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(amount || 0);
|
||||
}
|
||||
|
||||
function getNetIncomeClass(amount: number): string {
|
||||
if (amount > 0) return 'text-green-600';
|
||||
if (amount < 0) return 'text-red-600';
|
||||
return 'text-gray-600';
|
||||
}
|
||||
|
||||
// SEO
|
||||
useSeoMeta({
|
||||
title: "Budget Worksheet - Plan Your Co-op's Financial Future",
|
||||
description: "Interactive budget planning tool with multiple scenarios and multi-year projections for worker cooperatives.",
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue