refactor: streamline WizardPoliciesStep component layout and improve budget management forms with enhanced initial value handling
This commit is contained in:
parent
4cea1f71fe
commit
fc2d9ed56b
3 changed files with 272 additions and 267 deletions
|
|
@ -3,19 +3,13 @@
|
||||||
<!-- Section Header with Export Controls -->
|
<!-- Section Header with Export Controls -->
|
||||||
<div class="flex items-center justify-between mb-8">
|
<div class="flex items-center justify-between mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-2xl font-black text-black mb-2">
|
<h3 class="text-2xl font-black text-black mb-2">Set your wage & pay policy</h3>
|
||||||
Set your wage & pay policy
|
|
||||||
</h3>
|
|
||||||
<p class="text-neutral-600">
|
<p class="text-neutral-600">
|
||||||
Choose how to allocate payroll among members and set the base hourly rate.
|
Choose how to allocate payroll among members and set the base hourly rate.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<UButton
|
<UButton variant="outline" color="neutral" size="sm" @click="exportPolicies">
|
||||||
variant="outline"
|
|
||||||
color="gray"
|
|
||||||
size="sm"
|
|
||||||
@click="exportPolicies">
|
|
||||||
<UIcon name="i-heroicons-arrow-down-tray" class="mr-1" />
|
<UIcon name="i-heroicons-arrow-down-tray" class="mr-1" />
|
||||||
Export
|
Export
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
@ -26,7 +20,11 @@
|
||||||
<div class="p-6 border-3 border-black rounded-xl bg-white shadow-md">
|
<div class="p-6 border-3 border-black rounded-xl bg-white shadow-md">
|
||||||
<h4 class="font-bold mb-4">Pay Allocation Policy</h4>
|
<h4 class="font-bold mb-4">Pay Allocation Policy</h4>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<label v-for="option in policyOptions" :key="option.value" class="flex items-start gap-3 cursor-pointer hover:bg-gray-50 p-2 rounded-lg transition-colors">
|
<label
|
||||||
|
v-for="option in policyOptions"
|
||||||
|
:key="option.value"
|
||||||
|
class="flex items-start gap-3 cursor-pointer hover:bg-gray-50 p-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
|
|
@ -37,13 +35,17 @@
|
||||||
<span class="text-sm flex-1">{{ option.label }}</span>
|
<span class="text-sm flex-1">{{ option.label }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Role bands editor if role-banded is selected -->
|
<!-- Role bands editor if role-banded is selected -->
|
||||||
<div v-if="selectedPolicy === 'role-banded'" class="mt-4 p-4 bg-gray-50 rounded-lg">
|
<div v-if="selectedPolicy === 'role-banded'" class="mt-4 p-4 bg-gray-50 rounded-lg">
|
||||||
<h5 class="text-sm font-medium mb-3">Role Bands (monthly € or weight)</h5>
|
<h5 class="text-sm font-medium mb-3">Role Bands (monthly € or weight)</h5>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div v-for="member in uniqueRoles" :key="member.role" class="flex items-center gap-2">
|
<div
|
||||||
<span class="text-sm w-32">{{ member.role || 'No role' }}</span>
|
v-for="member in uniqueRoles"
|
||||||
|
:key="member.role"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="text-sm w-32">{{ member.role || "No role" }}</span>
|
||||||
<UInput
|
<UInput
|
||||||
v-model="roleBands[member.role || '']"
|
v-model="roleBands[member.role || '']"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -55,7 +57,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UAlert
|
<UAlert
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
@ -67,7 +69,7 @@
|
||||||
</template>
|
</template>
|
||||||
</UAlert>
|
</UAlert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hourly Wage Input -->
|
<!-- Hourly Wage Input -->
|
||||||
<div class="p-6 border-3 border-black rounded-xl bg-white shadow-md">
|
<div class="p-6 border-3 border-black rounded-xl bg-white shadow-md">
|
||||||
<h4 class="font-bold mb-4">Base Hourly Wage</h4>
|
<h4 class="font-bold mb-4">Base Hourly Wage</h4>
|
||||||
|
|
@ -78,7 +80,8 @@
|
||||||
placeholder="0.00"
|
placeholder="0.00"
|
||||||
size="xl"
|
size="xl"
|
||||||
class="text-4xl font-black w-full h-20"
|
class="text-4xl font-black w-full h-20"
|
||||||
@update:model-value="validateAndSaveWage">
|
@update:model-value="validateAndSaveWage"
|
||||||
|
>
|
||||||
<template #leading>
|
<template #leading>
|
||||||
<span class="text-neutral-500 text-3xl">€</span>
|
<span class="text-neutral-500 text-3xl">€</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -98,9 +101,9 @@ const coop = useCoopBuilder();
|
||||||
const store = useCoopBuilderStore();
|
const store = useCoopBuilderStore();
|
||||||
|
|
||||||
// Initialize from store
|
// Initialize from store
|
||||||
const selectedPolicy = ref(coop.policy.value?.relationship || 'equal-pay')
|
const selectedPolicy = ref(coop.policy.value?.relationship || "equal-pay");
|
||||||
const roleBands = ref(coop.policy.value?.roleBands || {})
|
const roleBands = ref(coop.policy.value?.roleBands || {});
|
||||||
const wageText = ref(String(store.equalHourlyWage || ''))
|
const wageText = ref(String(store.equalHourlyWage || ""));
|
||||||
|
|
||||||
function parseNumberInput(val: unknown): number {
|
function parseNumberInput(val: unknown): number {
|
||||||
if (typeof val === "number") return val;
|
if (typeof val === "number") return val;
|
||||||
|
|
@ -114,43 +117,54 @@ function parseNumberInput(val: unknown): number {
|
||||||
|
|
||||||
// Pay policy options
|
// Pay policy options
|
||||||
const policyOptions = [
|
const policyOptions = [
|
||||||
{ value: 'equal-pay', label: 'Equal pay - Everyone gets the same monthly amount' },
|
{
|
||||||
{ value: 'needs-weighted', label: 'Needs-weighted - Allocate based on minimum needs' },
|
value: "equal-pay",
|
||||||
{ value: 'hours-weighted', label: 'Hours-weighted - Allocate based on hours worked' },
|
label: "Equal pay - Everyone gets the same monthly amount",
|
||||||
{ value: 'role-banded', label: 'Role-banded - Different amounts per role' }
|
},
|
||||||
]
|
{
|
||||||
|
value: "needs-weighted",
|
||||||
|
label: "Needs-weighted - Allocate based on minimum needs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "hours-weighted",
|
||||||
|
label: "Hours-weighted - Allocate based on hours worked",
|
||||||
|
},
|
||||||
|
{ value: "role-banded", label: "Role-banded - Different amounts per role" },
|
||||||
|
];
|
||||||
|
|
||||||
// Already initialized above with store values
|
// Already initialized above with store values
|
||||||
|
|
||||||
const uniqueRoles = computed(() => {
|
const uniqueRoles = computed(() => {
|
||||||
const roles = new Set(coop.members.value.map(m => m.role || ''))
|
const roles = new Set(coop.members.value.map((m) => m.role || ""));
|
||||||
return Array.from(roles).map(role => ({ role }))
|
return Array.from(roles).map((role) => ({ role }));
|
||||||
})
|
});
|
||||||
|
|
||||||
function updatePolicy(value: string) {
|
function updatePolicy(value: string) {
|
||||||
selectedPolicy.value = value
|
selectedPolicy.value = value;
|
||||||
coop.setPolicy(value as "equal-pay" | "needs-weighted" | "hours-weighted" | "role-banded")
|
coop.setPolicy(
|
||||||
|
value as "equal-pay" | "needs-weighted" | "hours-weighted" | "role-banded"
|
||||||
|
);
|
||||||
|
|
||||||
// Trigger payroll reallocation after policy change
|
// Trigger payroll reallocation after policy change
|
||||||
const allocatedMembers = coop.allocatePayroll()
|
const allocatedMembers = coop.allocatePayroll();
|
||||||
allocatedMembers.forEach(m => {
|
allocatedMembers.forEach((m) => {
|
||||||
coop.upsertMember(m)
|
coop.upsertMember(m);
|
||||||
})
|
});
|
||||||
|
|
||||||
emit("save-status", "saved");
|
emit("save-status", "saved");
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRoleBands() {
|
function updateRoleBands() {
|
||||||
coop.setRoleBands(roleBands.value)
|
coop.setRoleBands(roleBands.value);
|
||||||
|
|
||||||
// Trigger payroll reallocation after role bands change
|
// Trigger payroll reallocation after role bands change
|
||||||
if (selectedPolicy.value === 'role-banded') {
|
if (selectedPolicy.value === "role-banded") {
|
||||||
const allocatedMembers = coop.allocatePayroll()
|
const allocatedMembers = coop.allocatePayroll();
|
||||||
allocatedMembers.forEach(m => {
|
allocatedMembers.forEach((m) => {
|
||||||
coop.upsertMember(m)
|
coop.upsertMember(m);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("save-status", "saved");
|
emit("save-status", "saved");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,14 +177,14 @@ function validateAndSaveWage(value: string) {
|
||||||
wageText.value = cleanValue;
|
wageText.value = cleanValue;
|
||||||
|
|
||||||
if (!isNaN(numValue) && numValue >= 0) {
|
if (!isNaN(numValue) && numValue >= 0) {
|
||||||
coop.setEqualWage(numValue)
|
coop.setEqualWage(numValue);
|
||||||
|
|
||||||
// Trigger payroll reallocation after wage change
|
// Trigger payroll reallocation after wage change
|
||||||
const allocatedMembers = coop.allocatePayroll()
|
const allocatedMembers = coop.allocatePayroll();
|
||||||
allocatedMembers.forEach(m => {
|
allocatedMembers.forEach((m) => {
|
||||||
coop.upsertMember(m)
|
coop.upsertMember(m);
|
||||||
})
|
});
|
||||||
|
|
||||||
emit("save-status", "saved");
|
emit("save-status", "saved");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
423
pages/budget.vue
423
pages/budget.vue
|
|
@ -95,15 +95,12 @@
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between group">
|
<div class="flex items-center justify-between group">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<button
|
<div class="text-left w-full">
|
||||||
@click="openHelperForItem(item)"
|
|
||||||
class="text-left hover:underline focus:outline-none focus:underline w-full"
|
|
||||||
>
|
|
||||||
<div class="font-medium">{{ item.name }}</div>
|
<div class="font-medium">{{ item.name }}</div>
|
||||||
<div class="text-xs text-gray-600">
|
<div class="text-xs text-gray-600">
|
||||||
{{ item.subcategory }}
|
{{ item.subcategory }}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UButton
|
<UButton
|
||||||
@click="removeItem('revenue', item.id)"
|
@click="removeItem('revenue', item.id)"
|
||||||
|
|
@ -204,15 +201,12 @@
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between group">
|
<div class="flex items-center justify-between group">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<button
|
<div class="text-left w-full">
|
||||||
@click="openHelperForItem(item)"
|
|
||||||
class="text-left hover:underline focus:outline-none focus:underline w-full"
|
|
||||||
>
|
|
||||||
<div class="font-medium">{{ item.name }}</div>
|
<div class="font-medium">{{ item.name }}</div>
|
||||||
<div class="text-xs text-gray-600">
|
<div class="text-xs text-gray-600">
|
||||||
{{ item.subcategory }}
|
{{ item.subcategory }}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UButton
|
<UButton
|
||||||
@click="removeItem('expenses', item.id)"
|
@click="removeItem('expenses', item.id)"
|
||||||
|
|
@ -308,11 +302,20 @@
|
||||||
<UFormGroup label="Category" required>
|
<UFormGroup label="Category" required>
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
v-model="newRevenue.category"
|
v-model="newRevenue.category"
|
||||||
:options="revenueCategories"
|
:items="revenueCategories"
|
||||||
placeholder="Select a category"
|
placeholder="Select a category"
|
||||||
/>
|
/>
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Subcategory" :required="false">
|
||||||
|
<USelectMenu
|
||||||
|
v-model="newRevenue.subcategory"
|
||||||
|
:items="revenueSubcategories"
|
||||||
|
placeholder="Select a subcategory"
|
||||||
|
:disabled="!newRevenue.category"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup label="Revenue Name" required>
|
<UFormGroup label="Revenue Name" required>
|
||||||
<UInput
|
<UInput
|
||||||
v-model="newRevenue.name"
|
v-model="newRevenue.name"
|
||||||
|
|
@ -321,20 +324,59 @@
|
||||||
/>
|
/>
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup
|
<div class="border-t-2 border-gray-200 pt-5">
|
||||||
label="Initial Monthly Amount"
|
<h4 class="text-sm font-semibold text-gray-700 mb-3">Initial Values</h4>
|
||||||
description="Starting amount for each month"
|
|
||||||
>
|
<UTabs v-model="revenueInitialTab" :items="revenueHelperTabs" class="w-full">
|
||||||
<UInput
|
<template #content="{ item }">
|
||||||
v-model.number="newRevenue.initialAmount"
|
<!-- Annual Distribution -->
|
||||||
type="number"
|
<div v-if="item.key === 'annual'" class="pt-4 space-y-4">
|
||||||
placeholder="0.00"
|
<UFormGroup label="Annual Total Amount">
|
||||||
>
|
<UInput
|
||||||
<template #leading>
|
v-model.number="newRevenue.annualAmount"
|
||||||
<span class="text-gray-500">$</span>
|
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 ${{ newRevenue.annualAmount || 0 }} equally across all 12 months
|
||||||
|
(${{ newRevenue.annualAmount ? Math.round(newRevenue.annualAmount / 12) : 0 }} per month)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Monthly Amount -->
|
||||||
|
<div v-else-if="item.key === 'monthly'" class="pt-4 space-y-4">
|
||||||
|
<UFormGroup label="Monthly Amount">
|
||||||
|
<UInput
|
||||||
|
v-model.number="newRevenue.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 ${{ newRevenue.monthlyAmount || 0 }} for all months
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Start Empty -->
|
||||||
|
<div v-else class="pt-4">
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
The revenue item will be created with no initial values. You can fill them in later.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UInput>
|
</UTabs>
|
||||||
</UFormGroup>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -356,79 +398,6 @@
|
||||||
</template>
|
</template>
|
||||||
</UModal>
|
</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 -->
|
<!-- Add Expense Modal -->
|
||||||
<UModal v-model:open="showAddExpenseModal">
|
<UModal v-model:open="showAddExpenseModal">
|
||||||
|
|
@ -453,7 +422,7 @@
|
||||||
<UFormGroup label="Category" required>
|
<UFormGroup label="Category" required>
|
||||||
<USelectMenu
|
<USelectMenu
|
||||||
v-model="newExpense.category"
|
v-model="newExpense.category"
|
||||||
:options="expenseCategories"
|
:items="expenseCategories"
|
||||||
placeholder="Select a category"
|
placeholder="Select a category"
|
||||||
/>
|
/>
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
@ -466,20 +435,59 @@
|
||||||
/>
|
/>
|
||||||
</UFormGroup>
|
</UFormGroup>
|
||||||
|
|
||||||
<UFormGroup
|
<div class="border-t-2 border-gray-200 pt-5">
|
||||||
label="Initial Monthly Amount"
|
<h4 class="text-sm font-semibold text-gray-700 mb-3">Initial Values</h4>
|
||||||
description="Starting amount for each month"
|
|
||||||
>
|
<UTabs v-model="expenseInitialTab" :items="expenseHelperTabs" class="w-full">
|
||||||
<UInput
|
<template #content="{ item }">
|
||||||
v-model.number="newExpense.initialAmount"
|
<!-- Annual Distribution -->
|
||||||
type="number"
|
<div v-if="item.key === 'annual'" class="pt-4 space-y-4">
|
||||||
placeholder="0.00"
|
<UFormGroup label="Annual Total Amount">
|
||||||
>
|
<UInput
|
||||||
<template #leading>
|
v-model.number="newExpense.annualAmount"
|
||||||
<span class="text-gray-500">$</span>
|
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 ${{ newExpense.annualAmount || 0 }} equally across all 12 months
|
||||||
|
(${{ newExpense.annualAmount ? Math.round(newExpense.annualAmount / 12) : 0 }} per month)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Monthly Amount -->
|
||||||
|
<div v-else-if="item.key === 'monthly'" class="pt-4 space-y-4">
|
||||||
|
<UFormGroup label="Monthly Amount">
|
||||||
|
<UInput
|
||||||
|
v-model.number="newExpense.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 ${{ newExpense.monthlyAmount || 0 }} for all months
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Start Empty -->
|
||||||
|
<div v-else class="pt-4">
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
The expense item will be created with no initial values. You can fill them in later.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UInput>
|
</UTabs>
|
||||||
</UFormGroup>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -513,7 +521,6 @@ const policiesStore = usePoliciesStore();
|
||||||
// State
|
// State
|
||||||
const showAddRevenueModal = ref(false);
|
const showAddRevenueModal = ref(false);
|
||||||
const showAddExpenseModal = ref(false);
|
const showAddExpenseModal = ref(false);
|
||||||
const showHelperModal = ref(false);
|
|
||||||
const activeTab = ref(0);
|
const activeTab = ref(0);
|
||||||
const highlightedItemId = ref<string | null>(null);
|
const highlightedItemId = ref<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -523,6 +530,8 @@ const newRevenue = ref({
|
||||||
subcategory: "",
|
subcategory: "",
|
||||||
name: "",
|
name: "",
|
||||||
initialAmount: 0,
|
initialAmount: 0,
|
||||||
|
annualAmount: 0,
|
||||||
|
monthlyAmount: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const newExpense = ref({
|
const newExpense = ref({
|
||||||
|
|
@ -530,27 +539,14 @@ const newExpense = ref({
|
||||||
subcategory: "",
|
subcategory: "",
|
||||||
name: "",
|
name: "",
|
||||||
initialAmount: 0,
|
initialAmount: 0,
|
||||||
});
|
|
||||||
|
|
||||||
// Helper config
|
|
||||||
const helperConfig = ref({
|
|
||||||
selectedItem: null as string | null,
|
|
||||||
annualAmount: 0,
|
annualAmount: 0,
|
||||||
monthlyAmount: 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
|
// New modal helper tabs
|
||||||
const activeHelperTab = ref(0); // UTabs uses index, not key
|
const revenueInitialTab = ref(0);
|
||||||
const helperTabs = [
|
const revenueHelperTabs = [
|
||||||
{
|
{
|
||||||
key: "annual",
|
key: "annual",
|
||||||
label: "Annual Distribution",
|
label: "Annual Distribution",
|
||||||
|
|
@ -561,12 +557,49 @@ const helperTabs = [
|
||||||
label: "Set All Months",
|
label: "Set All Months",
|
||||||
icon: "i-heroicons-squares-2x2",
|
icon: "i-heroicons-squares-2x2",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "empty",
|
||||||
|
label: "Start Empty",
|
||||||
|
icon: "i-heroicons-minus",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Data from store
|
const expenseInitialTab = ref(0);
|
||||||
|
const expenseHelperTabs = [
|
||||||
|
{
|
||||||
|
key: "annual",
|
||||||
|
label: "Annual Distribution",
|
||||||
|
icon: "i-heroicons-calendar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "monthly",
|
||||||
|
label: "Set All Months",
|
||||||
|
icon: "i-heroicons-squares-2x2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "empty",
|
||||||
|
label: "Start Empty",
|
||||||
|
icon: "i-heroicons-minus",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Data from store - just use the string arrays directly
|
||||||
const revenueCategories = computed(() => budgetStore.revenueCategories);
|
const revenueCategories = computed(() => budgetStore.revenueCategories);
|
||||||
const expenseCategories = computed(() => budgetStore.expenseCategories);
|
const expenseCategories = computed(() => budgetStore.expenseCategories);
|
||||||
|
|
||||||
|
// Revenue subcategories based on selected category
|
||||||
|
const revenueSubcategories = computed(() => {
|
||||||
|
if (!newRevenue.value.category) return [];
|
||||||
|
return budgetStore.revenueSubcategories[newRevenue.value.category] || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear subcategory when category changes
|
||||||
|
watch(() => newRevenue.value.category, (newCategory, oldCategory) => {
|
||||||
|
if (newCategory !== oldCategory) {
|
||||||
|
newRevenue.value.subcategory = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Generate monthly headers
|
// Generate monthly headers
|
||||||
const monthlyHeaders = computed(() => {
|
const monthlyHeaders = computed(() => {
|
||||||
const headers = [];
|
const headers = [];
|
||||||
|
|
@ -591,41 +624,11 @@ const groupedRevenue = computed(() => budgetStore.groupedRevenue);
|
||||||
const groupedExpenses = computed(() => budgetStore.groupedExpenses);
|
const groupedExpenses = computed(() => budgetStore.groupedExpenses);
|
||||||
const monthlyTotals = computed(() => budgetStore.monthlyTotals);
|
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
|
// Initialize on mount
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
// Always re-initialize to get latest wizard data
|
// Only initialize if not already done (preserve persisted data)
|
||||||
budgetStore.isInitialized = false;
|
|
||||||
await budgetStore.initializeFromWizardData();
|
await budgetStore.initializeFromWizardData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error initializing budget page:", error);
|
console.error("Error initializing budget page:", error);
|
||||||
|
|
@ -637,20 +640,36 @@ function addRevenueItem() {
|
||||||
const id = budgetStore.addBudgetItem(
|
const id = budgetStore.addBudgetItem(
|
||||||
"revenue",
|
"revenue",
|
||||||
newRevenue.value.name,
|
newRevenue.value.name,
|
||||||
newRevenue.value.category
|
newRevenue.value.category,
|
||||||
|
newRevenue.value.subcategory
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set initial amount for all months if provided
|
// Apply helper values based on selected tab
|
||||||
if (newRevenue.value.initialAmount > 0) {
|
const activeTab = revenueHelperTabs[revenueInitialTab.value];
|
||||||
|
|
||||||
|
if (activeTab?.key === 'annual' && Number(newRevenue.value.annualAmount) > 0) {
|
||||||
|
// Annual distribution
|
||||||
|
const monthlyAmount = Math.round(newRevenue.value.annualAmount / 12);
|
||||||
monthlyHeaders.value.forEach((month) => {
|
monthlyHeaders.value.forEach((month) => {
|
||||||
budgetStore.updateMonthlyValue(
|
budgetStore.updateMonthlyValue(
|
||||||
"revenue",
|
"revenue",
|
||||||
id,
|
id,
|
||||||
month.key,
|
month.key,
|
||||||
newRevenue.value.initialAmount.toString()
|
monthlyAmount.toString()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else if (activeTab?.key === 'monthly' && Number(newRevenue.value.monthlyAmount) > 0) {
|
||||||
|
// Set all months
|
||||||
|
monthlyHeaders.value.forEach((month) => {
|
||||||
|
budgetStore.updateMonthlyValue(
|
||||||
|
"revenue",
|
||||||
|
id,
|
||||||
|
month.key,
|
||||||
|
newRevenue.value.monthlyAmount.toString()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// If tab === 2 (empty), don't set any values
|
||||||
|
|
||||||
// Reset form and close modal
|
// Reset form and close modal
|
||||||
newRevenue.value = {
|
newRevenue.value = {
|
||||||
|
|
@ -658,7 +677,10 @@ function addRevenueItem() {
|
||||||
subcategory: "",
|
subcategory: "",
|
||||||
name: "",
|
name: "",
|
||||||
initialAmount: 0,
|
initialAmount: 0,
|
||||||
|
annualAmount: 0,
|
||||||
|
monthlyAmount: 0,
|
||||||
};
|
};
|
||||||
|
revenueInitialTab.value = 0;
|
||||||
showAddRevenueModal.value = false;
|
showAddRevenueModal.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -670,17 +692,32 @@ function addExpenseItem() {
|
||||||
newExpense.value.category
|
newExpense.value.category
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set initial amount for all months if provided
|
// Apply helper values based on selected tab
|
||||||
if (newExpense.value.initialAmount > 0) {
|
const activeExpenseTab = expenseHelperTabs[expenseInitialTab.value];
|
||||||
|
|
||||||
|
if (activeExpenseTab?.key === 'annual' && Number(newExpense.value.annualAmount) > 0) {
|
||||||
|
// Annual distribution
|
||||||
|
const monthlyAmount = Math.round(newExpense.value.annualAmount / 12);
|
||||||
monthlyHeaders.value.forEach((month) => {
|
monthlyHeaders.value.forEach((month) => {
|
||||||
budgetStore.updateMonthlyValue(
|
budgetStore.updateMonthlyValue(
|
||||||
"expenses",
|
"expenses",
|
||||||
id,
|
id,
|
||||||
month.key,
|
month.key,
|
||||||
newExpense.value.initialAmount.toString()
|
monthlyAmount.toString()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else if (activeExpenseTab?.key === 'monthly' && Number(newExpense.value.monthlyAmount) > 0) {
|
||||||
|
// Set all months
|
||||||
|
monthlyHeaders.value.forEach((month) => {
|
||||||
|
budgetStore.updateMonthlyValue(
|
||||||
|
"expenses",
|
||||||
|
id,
|
||||||
|
month.key,
|
||||||
|
newExpense.value.monthlyAmount.toString()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// If tab === 2 (empty), don't set any values
|
||||||
|
|
||||||
// Reset form and close modal
|
// Reset form and close modal
|
||||||
newExpense.value = {
|
newExpense.value = {
|
||||||
|
|
@ -688,7 +725,10 @@ function addExpenseItem() {
|
||||||
subcategory: "",
|
subcategory: "",
|
||||||
name: "",
|
name: "",
|
||||||
initialAmount: 0,
|
initialAmount: 0,
|
||||||
|
annualAmount: 0,
|
||||||
|
monthlyAmount: 0,
|
||||||
};
|
};
|
||||||
|
expenseInitialTab.value = 0;
|
||||||
showAddExpenseModal.value = false;
|
showAddExpenseModal.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -726,14 +766,6 @@ function handleEnter(event: KeyboardEvent) {
|
||||||
input.blur();
|
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
|
// Highlight row after changes
|
||||||
function highlightRow(itemId: string) {
|
function highlightRow(itemId: string) {
|
||||||
|
|
@ -743,47 +775,6 @@ function highlightRow(itemId: string) {
|
||||||
}, 1500);
|
}, 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
|
// Reset worksheet
|
||||||
function resetWorksheet() {
|
function resetWorksheet() {
|
||||||
|
|
|
||||||
|
|
@ -661,7 +661,7 @@ export const useBudgetStore = defineStore(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addBudgetItem(category, name, selectedCategory = "") {
|
function addBudgetItem(category, name, selectedCategory = "", subcategory = "") {
|
||||||
const id = `${category}-${Date.now()}`;
|
const id = `${category}-${Date.now()}`;
|
||||||
|
|
||||||
// Create empty monthly values for next 12 months
|
// Create empty monthly values for next 12 months
|
||||||
|
|
@ -681,7 +681,7 @@ export const useBudgetStore = defineStore(
|
||||||
mainCategory:
|
mainCategory:
|
||||||
selectedCategory ||
|
selectedCategory ||
|
||||||
(category === "revenue" ? "Games & Products" : "Other Expenses"),
|
(category === "revenue" ? "Games & Products" : "Other Expenses"),
|
||||||
subcategory: "", // Will be set by user via dropdown
|
subcategory: subcategory || "",
|
||||||
source: "user",
|
source: "user",
|
||||||
monthlyValues,
|
monthlyValues,
|
||||||
values: {
|
values: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue