app/pages/budget.vue

320 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<section class="py-8 space-y-6">
<div class="flex items-center justify-between">
<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>
<!-- Budget Worksheet Table -->
<UCard>
<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>
</section>
</template>
<script setup lang="ts">
// Import components explicitly
import BudgetCategorySelector from '~/components/BudgetCategorySelector.vue';
// Use budget worksheet store
const budgetStore = useBudgetStore();
// 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;
});
// 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);
});
// 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>