878 lines
27 KiB
Vue
878 lines
27 KiB
Vue
<template>
|
||
<div>
|
||
<section class="py-8 space-y-6">
|
||
<div class="flex items-center justify-between">
|
||
<h2 class="text-2xl font-bold">Budget Worksheet</h2>
|
||
<div class="flex items-center gap-2">
|
||
<UButton
|
||
@click="exportBudget"
|
||
variant="outline"
|
||
size="sm"
|
||
:ui="{
|
||
base:
|
||
'border-2 border-black hover:bg-black hover:text-white transition-none',
|
||
}"
|
||
>
|
||
Export
|
||
</UButton>
|
||
<UButton
|
||
@click="resetWorksheet"
|
||
variant="outline"
|
||
size="sm"
|
||
:ui="{
|
||
base:
|
||
'border-2 border-black hover:bg-black hover:text-white transition-none',
|
||
}"
|
||
>
|
||
Reset
|
||
</UButton>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Budget Table -->
|
||
<div class="border-4 border-black bg-white shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]">
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full border-collapse text-sm">
|
||
<thead>
|
||
<tr class="border-b-2 border-black">
|
||
<th
|
||
class="border-r-2 border-black px-4 py-3 text-left font-bold min-w-[250px] sticky left-0 bg-white z-10"
|
||
>
|
||
Item
|
||
</th>
|
||
<th
|
||
v-for="month in monthlyHeaders"
|
||
:key="month.key"
|
||
class="border-r border-gray-400 px-2 py-3 text-center font-medium min-w-[80px] last:border-r-0"
|
||
>
|
||
{{ month.label }}
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<!-- REVENUE SECTION -->
|
||
<tr class="bg-black text-white">
|
||
<td class="px-4 py-2 font-bold sticky left-0 bg-black z-10">
|
||
<div class="flex items-center justify-between">
|
||
<span>REVENUE</span>
|
||
<UButton
|
||
@click="showAddRevenueModal = true"
|
||
size="xs"
|
||
:ui="{
|
||
base: 'bg-white text-black hover:bg-gray-200 transition-none',
|
||
}"
|
||
>
|
||
+ Add
|
||
</UButton>
|
||
</div>
|
||
</td>
|
||
<td class="px-2 py-2" :colspan="monthlyHeaders.length"></td>
|
||
</tr>
|
||
|
||
<!-- Revenue by Category -->
|
||
<template
|
||
v-for="(items, categoryName) in groupedRevenue"
|
||
:key="`revenue-${categoryName}`"
|
||
>
|
||
<tr v-if="items.length > 0" class="border-t border-gray-300">
|
||
<td
|
||
class="px-4 py-1 font-semibold sticky left-0 bg-gray-100 z-10 border-r-2 border-black"
|
||
:colspan="monthlyHeaders.length + 1"
|
||
>
|
||
{{ categoryName }}
|
||
</td>
|
||
</tr>
|
||
<tr
|
||
v-for="item in items"
|
||
:key="item.id"
|
||
:class="[
|
||
'border-t border-gray-200 hover:bg-gray-50 transition-all duration-300',
|
||
highlightedItemId === item.id && 'bg-yellow-100 animate-pulse',
|
||
]"
|
||
>
|
||
<td
|
||
class="border-r-2 border-black px-4 py-2 sticky left-0 bg-white z-10"
|
||
>
|
||
<div class="flex items-center justify-between group">
|
||
<div class="flex-1">
|
||
<button
|
||
@click="openHelperForItem(item)"
|
||
class="text-left hover:underline focus:outline-none focus:underline w-full"
|
||
>
|
||
<div class="font-medium">{{ item.name }}</div>
|
||
<div class="text-xs text-gray-600">
|
||
{{ item.subcategory }}
|
||
</div>
|
||
</button>
|
||
</div>
|
||
<UButton
|
||
@click="removeItem('revenue', item.id)"
|
||
size="xs"
|
||
variant="ghost"
|
||
:ui="{
|
||
base:
|
||
'text-red-600 hover:bg-red-100 opacity-0 group-hover:opacity-100 transition-none',
|
||
}"
|
||
>
|
||
×
|
||
</UButton>
|
||
</div>
|
||
</td>
|
||
<td
|
||
v-for="month in monthlyHeaders"
|
||
:key="month.key"
|
||
class="border-r border-gray-200 px-1 py-1 last:border-r-0"
|
||
>
|
||
<input
|
||
type="text"
|
||
:value="formatValue(item.monthlyValues?.[month.key] || 0)"
|
||
@focus="handleFocus($event)"
|
||
@blur="handleBlur($event, 'revenue', item.id, month.key)"
|
||
@keydown.enter="handleEnter($event)"
|
||
class="w-full text-right px-1 py-0.5 border-2 border-transparent hover:border-gray-400 focus:border-black focus:outline-none transition-none"
|
||
:class="{
|
||
'bg-gray-50': !item.monthlyValues?.[month.key],
|
||
}"
|
||
/>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
|
||
<!-- Total Revenue Row -->
|
||
<tr class="border-t-2 border-black font-bold bg-gray-100">
|
||
<td
|
||
class="border-r-2 border-black px-4 py-2 sticky left-0 bg-gray-100 z-10"
|
||
>
|
||
TOTAL REVENUE
|
||
</td>
|
||
<td
|
||
v-for="month in monthlyHeaders"
|
||
:key="month.key"
|
||
class="border-r border-gray-400 px-2 py-2 text-right last:border-r-0"
|
||
>
|
||
{{ formatCurrency(monthlyTotals[month.key]?.revenue || 0) }}
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Spacer -->
|
||
<tr>
|
||
<td colspan="100" class="h-4"></td>
|
||
</tr>
|
||
|
||
<!-- EXPENSES SECTION -->
|
||
<tr class="bg-black text-white">
|
||
<td class="px-4 py-2 font-bold sticky left-0 bg-black z-10">
|
||
<div class="flex items-center justify-between">
|
||
<span>EXPENSES</span>
|
||
<UButton
|
||
@click="showAddExpenseModal = true"
|
||
size="xs"
|
||
:ui="{
|
||
base: 'bg-white text-black hover:bg-gray-200 transition-none',
|
||
}"
|
||
>
|
||
+ Add
|
||
</UButton>
|
||
</div>
|
||
</td>
|
||
<td class="px-2 py-2" :colspan="monthlyHeaders.length"></td>
|
||
</tr>
|
||
|
||
<!-- Expenses by Category -->
|
||
<template
|
||
v-for="(items, categoryName) in groupedExpenses"
|
||
:key="`expense-${categoryName}`"
|
||
>
|
||
<tr v-if="items.length > 0" class="border-t border-gray-300">
|
||
<td
|
||
class="px-4 py-1 font-semibold sticky left-0 bg-gray-100 z-10 border-r-2 border-black"
|
||
:colspan="monthlyHeaders.length + 1"
|
||
>
|
||
{{ categoryName }}
|
||
</td>
|
||
</tr>
|
||
<tr
|
||
v-for="item in items"
|
||
:key="item.id"
|
||
:class="[
|
||
'border-t border-gray-200 hover:bg-gray-50 transition-all duration-300',
|
||
highlightedItemId === item.id && 'bg-yellow-100 animate-pulse',
|
||
]"
|
||
>
|
||
<td
|
||
class="border-r-2 border-black px-4 py-2 sticky left-0 bg-white z-10"
|
||
>
|
||
<div class="flex items-center justify-between group">
|
||
<div class="flex-1">
|
||
<button
|
||
@click="openHelperForItem(item)"
|
||
class="text-left hover:underline focus:outline-none focus:underline w-full"
|
||
>
|
||
<div class="font-medium">{{ item.name }}</div>
|
||
<div class="text-xs text-gray-600">
|
||
{{ item.subcategory }}
|
||
</div>
|
||
</button>
|
||
</div>
|
||
<UButton
|
||
@click="removeItem('expenses', item.id)"
|
||
size="xs"
|
||
variant="ghost"
|
||
:ui="{
|
||
base:
|
||
'text-red-600 hover:bg-red-100 opacity-0 group-hover:opacity-100 transition-none',
|
||
}"
|
||
>
|
||
×
|
||
</UButton>
|
||
</div>
|
||
</td>
|
||
<td
|
||
v-for="month in monthlyHeaders"
|
||
:key="month.key"
|
||
class="border-r border-gray-200 px-1 py-1 last:border-r-0"
|
||
>
|
||
<input
|
||
type="text"
|
||
:value="formatValue(item.monthlyValues?.[month.key] || 0)"
|
||
@focus="handleFocus($event)"
|
||
@blur="handleBlur($event, 'expenses', item.id, month.key)"
|
||
@keydown.enter="handleEnter($event)"
|
||
class="w-full text-right px-1 py-0.5 border-2 border-transparent hover:border-gray-400 focus:border-black focus:outline-none transition-none"
|
||
:class="{
|
||
'bg-gray-50': !item.monthlyValues?.[month.key],
|
||
}"
|
||
/>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
|
||
<!-- Total Expenses Row -->
|
||
<tr class="border-t-2 border-black font-bold bg-gray-100">
|
||
<td
|
||
class="border-r-2 border-black px-4 py-2 sticky left-0 bg-gray-100 z-10"
|
||
>
|
||
TOTAL EXPENSES
|
||
</td>
|
||
<td
|
||
v-for="month in monthlyHeaders"
|
||
:key="month.key"
|
||
class="border-r border-gray-400 px-2 py-2 text-right last:border-r-0"
|
||
>
|
||
{{ formatCurrency(monthlyTotals[month.key]?.expenses || 0) }}
|
||
</td>
|
||
</tr>
|
||
|
||
<!-- Net Income Row -->
|
||
<tr class="border-t-2 border-black font-bold text-lg">
|
||
<td class="border-r-2 border-black px-4 py-3 sticky left-0 bg-white z-10">
|
||
NET INCOME
|
||
</td>
|
||
<td
|
||
v-for="month in monthlyHeaders"
|
||
:key="month.key"
|
||
class="border-r border-gray-400 px-2 py-3 text-right last:border-r-0"
|
||
:class="getNetIncomeClass(monthlyTotals[month.key]?.net || 0)"
|
||
>
|
||
{{ formatCurrency(monthlyTotals[month.key]?.net || 0) }}
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Add Revenue Modal -->
|
||
<UModal v-model:open="showAddRevenueModal">
|
||
<template #header>
|
||
<div class="flex items-center justify-between border-b-4 border-black pb-4">
|
||
<div>
|
||
<h3 class="text-xl font-bold text-black">Add Revenue Source</h3>
|
||
<p class="mt-1 text-sm text-gray-600">
|
||
Create a new revenue stream for your budget
|
||
</p>
|
||
</div>
|
||
<UButton
|
||
@click="showAddRevenueModal = false"
|
||
variant="ghost"
|
||
size="sm"
|
||
icon="i-heroicons-x-mark"
|
||
class="text-gray-500 hover:text-black"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<template #body>
|
||
<div class="space-y-5 py-4">
|
||
<UFormGroup label="Category" required>
|
||
<USelectMenu
|
||
v-model="newRevenue.category"
|
||
:options="revenueCategories"
|
||
placeholder="Select a category"
|
||
/>
|
||
</UFormGroup>
|
||
|
||
<UFormGroup label="Revenue Name" required>
|
||
<UInput
|
||
v-model="newRevenue.name"
|
||
type="text"
|
||
placeholder="e.g., Monthly Subscription"
|
||
/>
|
||
</UFormGroup>
|
||
|
||
<UFormGroup
|
||
label="Initial Monthly Amount"
|
||
description="Starting amount for each month"
|
||
>
|
||
<UInput
|
||
v-model.number="newRevenue.initialAmount"
|
||
type="number"
|
||
placeholder="0.00"
|
||
>
|
||
<template #leading>
|
||
<span class="text-gray-500">$</span>
|
||
</template>
|
||
</UInput>
|
||
</UFormGroup>
|
||
</div>
|
||
</template>
|
||
|
||
<template #footer>
|
||
<div class="flex justify-end gap-3 border-t-2 border-gray-200 pt-4">
|
||
<UButton @click="showAddRevenueModal = false" variant="outline" size="md">
|
||
Cancel
|
||
</UButton>
|
||
<UButton
|
||
@click="addRevenueItem"
|
||
size="md"
|
||
:disabled="!newRevenue.name || !newRevenue.category"
|
||
color="primary"
|
||
>
|
||
<UIcon name="i-heroicons-plus" class="mr-1" />
|
||
Add Revenue
|
||
</UButton>
|
||
</div>
|
||
</template>
|
||
</UModal>
|
||
|
||
<!-- Helper Modal -->
|
||
<UModal
|
||
v-model:open="showHelperModal"
|
||
:ui="{ wrapper: 'sm:max-w-lg', footer: 'justify-end' }"
|
||
title="Quick Entry Tools"
|
||
:description="selectedItemDetails?.label || 'Budget item'"
|
||
>
|
||
<template #body>
|
||
<div class="isolate">
|
||
<UTabs v-model="activeHelperTab" :items="helperTabs" class="w-full">
|
||
<template #content="{ item }">
|
||
<!-- Annual Distribution Content -->
|
||
<div v-if="item.key === 'annual'" class="pt-4 space-y-4">
|
||
<UFormGroup label="Annual Total Amount">
|
||
<UInput
|
||
v-model.number="helperConfig.annualAmount"
|
||
type="number"
|
||
placeholder="Enter annual amount (e.g., 12000)"
|
||
size="lg"
|
||
>
|
||
<template #leading>
|
||
<span class="text-gray-500">$</span>
|
||
</template>
|
||
</UInput>
|
||
</UFormGroup>
|
||
<p class="text-sm text-gray-600">
|
||
This will divide the amount equally across all 12 months
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Monthly Amount Content -->
|
||
<div v-else-if="item.key === 'monthly'" class="pt-4 space-y-4">
|
||
<UFormGroup label="Monthly Amount">
|
||
<UInput
|
||
v-model.number="helperConfig.monthlyAmount"
|
||
type="number"
|
||
placeholder="Enter monthly amount (e.g., 1000)"
|
||
size="lg"
|
||
>
|
||
<template #leading>
|
||
<span class="text-gray-500">$</span>
|
||
</template>
|
||
</UInput>
|
||
</UFormGroup>
|
||
<p class="text-sm text-gray-600">
|
||
This will set the same value for all months
|
||
</p>
|
||
</div>
|
||
</template>
|
||
</UTabs>
|
||
</div>
|
||
</template>
|
||
|
||
<template #footer="{ close }">
|
||
<UButton @click="close" variant="outline" color="neutral"> Cancel </UButton>
|
||
<UButton
|
||
v-if="activeHelperTab === 0"
|
||
@click="distributeAnnualAmount"
|
||
:disabled="!helperConfig.annualAmount || helperConfig.annualAmount <= 0"
|
||
color="primary"
|
||
>
|
||
Distribute Amount
|
||
</UButton>
|
||
<UButton
|
||
v-else
|
||
@click="setAllMonths"
|
||
:disabled="!helperConfig.monthlyAmount || helperConfig.monthlyAmount <= 0"
|
||
color="primary"
|
||
>
|
||
Apply to All Months
|
||
</UButton>
|
||
</template>
|
||
</UModal>
|
||
|
||
<!-- Add Expense Modal -->
|
||
<UModal v-model:open="showAddExpenseModal">
|
||
<template #header>
|
||
<div class="flex items-center justify-between border-b-4 border-black pb-4">
|
||
<div>
|
||
<h3 class="text-xl font-bold text-black">Add Expense Item</h3>
|
||
<p class="mt-1 text-sm text-gray-600">Create a new expense for your budget</p>
|
||
</div>
|
||
<UButton
|
||
@click="showAddExpenseModal = false"
|
||
variant="ghost"
|
||
size="sm"
|
||
icon="i-heroicons-x-mark"
|
||
class="text-gray-500 hover:text-black"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<template #body>
|
||
<div class="space-y-5 py-4">
|
||
<UFormGroup label="Category" required>
|
||
<USelectMenu
|
||
v-model="newExpense.category"
|
||
:options="expenseCategories"
|
||
placeholder="Select a category"
|
||
/>
|
||
</UFormGroup>
|
||
|
||
<UFormGroup label="Expense Name" required>
|
||
<UInput
|
||
v-model="newExpense.name"
|
||
type="text"
|
||
placeholder="e.g., Office Rent"
|
||
/>
|
||
</UFormGroup>
|
||
|
||
<UFormGroup
|
||
label="Initial Monthly Amount"
|
||
description="Starting amount for each month"
|
||
>
|
||
<UInput
|
||
v-model.number="newExpense.initialAmount"
|
||
type="number"
|
||
placeholder="0.00"
|
||
>
|
||
<template #leading>
|
||
<span class="text-gray-500">$</span>
|
||
</template>
|
||
</UInput>
|
||
</UFormGroup>
|
||
</div>
|
||
</template>
|
||
|
||
<template #footer>
|
||
<div class="flex justify-end gap-3 border-t-2 border-gray-200 pt-4">
|
||
<UButton @click="showAddExpenseModal = false" variant="outline" size="md">
|
||
Cancel
|
||
</UButton>
|
||
<UButton
|
||
@click="addExpenseItem"
|
||
size="md"
|
||
:disabled="!newExpense.name || !newExpense.category"
|
||
color="primary"
|
||
>
|
||
<UIcon name="i-heroicons-plus" class="mr-1" />
|
||
Add Expense
|
||
</UButton>
|
||
</div>
|
||
</template>
|
||
</UModal>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
// Stores
|
||
const budgetStore = useBudgetStore();
|
||
const streamsStore = useStreamsStore();
|
||
const membersStore = useMembersStore();
|
||
const policiesStore = usePoliciesStore();
|
||
|
||
// State
|
||
const showAddRevenueModal = ref(false);
|
||
const showAddExpenseModal = ref(false);
|
||
const showHelperModal = ref(false);
|
||
const activeTab = ref(0);
|
||
const highlightedItemId = ref<string | null>(null);
|
||
|
||
// New item forms
|
||
const newRevenue = ref({
|
||
category: "",
|
||
subcategory: "",
|
||
name: "",
|
||
initialAmount: 0,
|
||
});
|
||
|
||
const newExpense = ref({
|
||
category: "",
|
||
subcategory: "",
|
||
name: "",
|
||
initialAmount: 0,
|
||
});
|
||
|
||
// Helper config
|
||
const helperConfig = ref({
|
||
selectedItem: null as string | null,
|
||
annualAmount: 0,
|
||
monthlyAmount: 0,
|
||
startAmount: 0,
|
||
percentChange: 0,
|
||
baseAmount: 0,
|
||
});
|
||
|
||
// Selected item details for helper modal
|
||
const selectedItemDetails = computed(() => {
|
||
if (!helperConfig.value.selectedItem) return null;
|
||
return allBudgetItems.value.find((item) => item.id === helperConfig.value.selectedItem);
|
||
});
|
||
|
||
// Helper tabs configuration
|
||
const activeHelperTab = ref(0); // UTabs uses index, not key
|
||
const helperTabs = [
|
||
{
|
||
key: "annual",
|
||
label: "Annual Distribution",
|
||
icon: "i-heroicons-calendar",
|
||
},
|
||
{
|
||
key: "monthly",
|
||
label: "Set All Months",
|
||
icon: "i-heroicons-squares-2x2",
|
||
},
|
||
];
|
||
|
||
// Data from store
|
||
const revenueCategories = computed(() => budgetStore.revenueCategories);
|
||
const expenseCategories = computed(() => budgetStore.expenseCategories);
|
||
|
||
// Generate monthly headers
|
||
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().toString().slice(-2);
|
||
|
||
headers.push({
|
||
key: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`,
|
||
label: `${monthName} '${year}`,
|
||
});
|
||
}
|
||
|
||
return headers;
|
||
});
|
||
|
||
// Grouped data
|
||
const groupedRevenue = computed(() => budgetStore.groupedRevenue);
|
||
const groupedExpenses = computed(() => budgetStore.groupedExpenses);
|
||
const monthlyTotals = computed(() => budgetStore.monthlyTotals);
|
||
|
||
// All budget items for helper dropdown
|
||
const allBudgetItems = computed(() => {
|
||
const items: Array<{
|
||
id: string;
|
||
label: string;
|
||
type: "revenue" | "expenses";
|
||
data: any;
|
||
}> = [];
|
||
|
||
budgetStore.budgetWorksheet.revenue.forEach((item: any) => {
|
||
items.push({
|
||
id: item.id,
|
||
label: `[Revenue] ${item.name}`,
|
||
type: "revenue",
|
||
data: item,
|
||
});
|
||
});
|
||
|
||
budgetStore.budgetWorksheet.expenses.forEach((item: any) => {
|
||
items.push({
|
||
id: item.id,
|
||
label: `[Expense] ${item.name}`,
|
||
type: "expenses",
|
||
data: item,
|
||
});
|
||
});
|
||
|
||
return items;
|
||
});
|
||
|
||
// Initialize on mount
|
||
onMounted(async () => {
|
||
try {
|
||
// Always re-initialize to get latest wizard data
|
||
budgetStore.isInitialized = false;
|
||
await budgetStore.initializeFromWizardData();
|
||
} catch (error) {
|
||
console.error("Error initializing budget page:", error);
|
||
}
|
||
});
|
||
|
||
// Add revenue item
|
||
function addRevenueItem() {
|
||
const id = budgetStore.addBudgetItem(
|
||
"revenue",
|
||
newRevenue.value.name,
|
||
newRevenue.value.category
|
||
);
|
||
|
||
// Set initial amount for all months if provided
|
||
if (newRevenue.value.initialAmount > 0) {
|
||
monthlyHeaders.value.forEach((month) => {
|
||
budgetStore.updateMonthlyValue(
|
||
"revenue",
|
||
id,
|
||
month.key,
|
||
newRevenue.value.initialAmount.toString()
|
||
);
|
||
});
|
||
}
|
||
|
||
// Reset form and close modal
|
||
newRevenue.value = {
|
||
category: "",
|
||
subcategory: "",
|
||
name: "",
|
||
initialAmount: 0,
|
||
};
|
||
showAddRevenueModal.value = false;
|
||
}
|
||
|
||
// Add expense item
|
||
function addExpenseItem() {
|
||
const id = budgetStore.addBudgetItem(
|
||
"expenses",
|
||
newExpense.value.name,
|
||
newExpense.value.category
|
||
);
|
||
|
||
// Set initial amount for all months if provided
|
||
if (newExpense.value.initialAmount > 0) {
|
||
monthlyHeaders.value.forEach((month) => {
|
||
budgetStore.updateMonthlyValue(
|
||
"expenses",
|
||
id,
|
||
month.key,
|
||
newExpense.value.initialAmount.toString()
|
||
);
|
||
});
|
||
}
|
||
|
||
// Reset form and close modal
|
||
newExpense.value = {
|
||
category: "",
|
||
subcategory: "",
|
||
name: "",
|
||
initialAmount: 0,
|
||
};
|
||
showAddExpenseModal.value = false;
|
||
}
|
||
|
||
// Remove item
|
||
function removeItem(category: string, itemId: string) {
|
||
if (confirm("Are you sure you want to remove this item?")) {
|
||
budgetStore.removeBudgetItem(category, itemId);
|
||
}
|
||
}
|
||
|
||
// Value entry handlers
|
||
function formatValue(value: number): string {
|
||
if (value === 0) return "";
|
||
return value.toString();
|
||
}
|
||
|
||
function handleFocus(event: FocusEvent) {
|
||
const input = event.target as HTMLInputElement;
|
||
input.select();
|
||
}
|
||
|
||
function handleBlur(
|
||
event: FocusEvent,
|
||
category: string,
|
||
itemId: string,
|
||
monthKey: string
|
||
) {
|
||
const input = event.target as HTMLInputElement;
|
||
const value = input.value.replace(/[^0-9.-]/g, "");
|
||
budgetStore.updateMonthlyValue(category, itemId, monthKey, value);
|
||
}
|
||
|
||
function handleEnter(event: KeyboardEvent) {
|
||
const input = event.target as HTMLInputElement;
|
||
input.blur();
|
||
}
|
||
|
||
// Open helper modal for specific item
|
||
function openHelperForItem(item: any) {
|
||
helperConfig.value.selectedItem = item.id;
|
||
helperConfig.value.annualAmount = 0;
|
||
helperConfig.value.monthlyAmount = 0;
|
||
activeTab.value = 0; // Reset to first tab
|
||
showHelperModal.value = true;
|
||
}
|
||
|
||
// Highlight row after changes
|
||
function highlightRow(itemId: string) {
|
||
highlightedItemId.value = itemId;
|
||
setTimeout(() => {
|
||
highlightedItemId.value = null;
|
||
}, 1500);
|
||
}
|
||
|
||
// Helper functions
|
||
function distributeAnnualAmount() {
|
||
if (!helperConfig.value.selectedItem || !helperConfig.value.annualAmount) return;
|
||
|
||
const item = allBudgetItems.value.find((i) => i.id === helperConfig.value.selectedItem);
|
||
if (!item) return;
|
||
|
||
const monthlyAmount = Math.round(helperConfig.value.annualAmount / 12);
|
||
monthlyHeaders.value.forEach((month) => {
|
||
budgetStore.updateMonthlyValue(
|
||
item.type,
|
||
item.id,
|
||
month.key,
|
||
monthlyAmount.toString()
|
||
);
|
||
});
|
||
|
||
helperConfig.value.annualAmount = 0;
|
||
highlightRow(item.id);
|
||
showHelperModal.value = false;
|
||
}
|
||
|
||
function setAllMonths() {
|
||
if (!helperConfig.value.selectedItem || !helperConfig.value.monthlyAmount) return;
|
||
|
||
const item = allBudgetItems.value.find((i) => i.id === helperConfig.value.selectedItem);
|
||
if (!item) return;
|
||
|
||
monthlyHeaders.value.forEach((month) => {
|
||
budgetStore.updateMonthlyValue(
|
||
item.type,
|
||
item.id,
|
||
month.key,
|
||
helperConfig.value.monthlyAmount.toString()
|
||
);
|
||
});
|
||
|
||
helperConfig.value.monthlyAmount = 0;
|
||
highlightRow(item.id);
|
||
showHelperModal.value = false;
|
||
}
|
||
|
||
// Reset worksheet
|
||
function resetWorksheet() {
|
||
if (confirm("Are you sure you want to reset all budget data? This cannot be undone.")) {
|
||
budgetStore.resetBudgetWorksheet();
|
||
budgetStore.isInitialized = false;
|
||
nextTick(() => {
|
||
budgetStore.initializeFromWizardData();
|
||
});
|
||
}
|
||
}
|
||
|
||
// Export budget
|
||
function exportBudget() {
|
||
const data = {
|
||
worksheet: budgetStore.budgetWorksheet,
|
||
monthlyTotals: budgetStore.monthlyTotals,
|
||
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);
|
||
}
|
||
|
||
// Formatting helpers
|
||
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 font-bold";
|
||
if (amount < 0) return "text-red-600 font-bold";
|
||
return "text-gray-600";
|
||
}
|
||
|
||
// SEO
|
||
useSeoMeta({
|
||
title: "Budget Worksheet - Plan Your Co-op's Financial Future",
|
||
description:
|
||
"Interactive budget planning tool with monthly projections for worker cooperatives.",
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* Ensure modal content doesn't inherit global styles */
|
||
.isolate {
|
||
isolation: isolate;
|
||
}
|
||
|
||
.modal-header {
|
||
text-decoration: none !important;
|
||
border-bottom: none !important;
|
||
}
|
||
|
||
.isolate h1,
|
||
.isolate h2,
|
||
.isolate h3,
|
||
.isolate h4,
|
||
.isolate h5,
|
||
.isolate h6 {
|
||
text-decoration: none !important;
|
||
border-bottom: none !important;
|
||
}
|
||
|
||
.isolate p {
|
||
text-decoration: none !important;
|
||
}
|
||
/* Remove number input spinners */
|
||
input[type="number"]::-webkit-inner-spin-button,
|
||
input[type="number"]::-webkit-outer-spin-button {
|
||
-webkit-appearance: none;
|
||
margin: 0;
|
||
}
|
||
|
||
input[type="number"] {
|
||
-moz-appearance: textfield;
|
||
}
|
||
</style>
|