refactor: enhance AnnualBudget component layout with improved dark mode support, streamline table structure, and update CSS for better visual consistency
This commit is contained in:
parent
24e8b7a3a8
commit
f073f91569
14 changed files with 1440 additions and 922 deletions
208
components/BudgetSettingsModal.vue
Normal file
208
components/BudgetSettingsModal.vue
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
<template>
|
||||
<UModal
|
||||
v-model:open="isOpen"
|
||||
title="Budget Settings"
|
||||
description="Configure payroll taxes and cash flow settings"
|
||||
:dismissible="true">
|
||||
<template #body>
|
||||
<div class="space-y-8">
|
||||
<!-- Payroll Tax Settings -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">
|
||||
Payroll Tax Percentage
|
||||
</h3>
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
|
||||
Set the percentage added to base payroll for taxes, benefits, and other oncosts.
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<UFormField label="Tax Percentage">
|
||||
<UInput
|
||||
v-model="newOncostPct"
|
||||
type="number"
|
||||
placeholder="Enter percentage"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
size="lg"
|
||||
class="text-lg">
|
||||
<template #trailing>
|
||||
<span class="text-neutral-500 text-lg">%</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
|
||||
<!-- Quick selection buttons -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="text-sm text-neutral-600 dark:text-neutral-400 mr-2">Quick select:</span>
|
||||
<UButton
|
||||
v-for="range in commonOncostRanges"
|
||||
:key="range.value"
|
||||
@click="newOncostPct = range.value"
|
||||
size="xs"
|
||||
variant="outline"
|
||||
:ui="{ base: 'hover:bg-neutral-100 dark:hover:bg-neutral-800' }">
|
||||
{{ range.value }}%
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="p-3 bg-neutral-100 dark:bg-neutral-800 rounded">
|
||||
<p class="text-sm text-neutral-700 dark:text-neutral-300">
|
||||
<strong>Example:</strong> €100 base payroll + {{ newOncostPct }}% tax =
|
||||
<strong>€{{ Math.round(100 * (1 + newOncostPct / 100)) }} total cost</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Minimum Cash Threshold Settings -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">
|
||||
Minimum Cash Threshold
|
||||
</h3>
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
|
||||
Set the minimum cash balance that your co-op should never go below. The budget will automatically reduce payroll allocations if needed to maintain this threshold.
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<UFormField label="Minimum Cash Balance">
|
||||
<UInput
|
||||
v-model="newMinCashThreshold"
|
||||
type="number"
|
||||
placeholder="Enter minimum amount"
|
||||
min="0"
|
||||
step="100"
|
||||
size="lg"
|
||||
class="text-lg">
|
||||
<template #leading>
|
||||
<span class="text-neutral-500 text-lg">{{ getCurrencySymbol(coopStore.currency) }}</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
|
||||
<!-- Quick selection buttons -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="text-sm text-neutral-600 dark:text-neutral-400 mr-2">Quick select:</span>
|
||||
<UButton
|
||||
v-for="amount in commonCashThresholds"
|
||||
:key="amount"
|
||||
@click="newMinCashThreshold = amount"
|
||||
size="xs"
|
||||
variant="outline"
|
||||
:ui="{ base: 'hover:bg-neutral-100 dark:hover:bg-neutral-800' }">
|
||||
{{ getCurrencySymbol(coopStore.currency) }}{{ formatAmount(amount) }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="p-3 bg-neutral-100 dark:bg-neutral-800 rounded">
|
||||
<p class="text-sm text-neutral-700 dark:text-neutral-300">
|
||||
<strong>Safety buffer:</strong> Your co-op will maintain at least
|
||||
<strong>{{ getCurrencySymbol(coopStore.currency) }}{{ formatAmount(newMinCashThreshold) }}</strong>
|
||||
in cash at all times.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<UButton
|
||||
@click="resetToDefaults"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
:ui="{ base: 'hover:bg-neutral-100 dark:hover:bg-neutral-800' }">
|
||||
Reset to Defaults
|
||||
</UButton>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<UButton
|
||||
@click="isOpen = false"
|
||||
variant="outline"
|
||||
color="neutral">
|
||||
Cancel
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="saveSettings"
|
||||
:disabled="!isValidSettings"
|
||||
color="primary">
|
||||
Save Settings
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getCurrencySymbol } from "~/utils/currency";
|
||||
|
||||
// Props
|
||||
const isOpen = defineModel<boolean>("open", { default: false });
|
||||
|
||||
// Emit events for parent component
|
||||
const emit = defineEmits<{
|
||||
"settings-updated": [];
|
||||
}>();
|
||||
|
||||
// Stores
|
||||
const coopStore = useCoopBuilderStore();
|
||||
|
||||
// Reactive form data
|
||||
const newOncostPct = ref(coopStore.payrollOncostPct || 25);
|
||||
const newMinCashThreshold = ref(coopStore.minCashThreshold || 0);
|
||||
|
||||
// Common ranges for quick selection
|
||||
const commonOncostRanges = [
|
||||
{ value: 0 },
|
||||
{ value: 15 },
|
||||
{ value: 20 },
|
||||
{ value: 25 },
|
||||
{ value: 30 },
|
||||
{ value: 35 }
|
||||
];
|
||||
|
||||
const commonCashThresholds = [0, 1000, 2500, 5000, 7500, 10000];
|
||||
|
||||
// Computed properties
|
||||
const isValidSettings = computed(() =>
|
||||
newOncostPct.value >= 0 &&
|
||||
newOncostPct.value <= 100 &&
|
||||
newMinCashThreshold.value >= 0
|
||||
);
|
||||
|
||||
// Utility function to format amounts
|
||||
function formatAmount(amount: number): string {
|
||||
return new Intl.NumberFormat().format(amount);
|
||||
}
|
||||
|
||||
// Reset form when modal opens
|
||||
watch(() => isOpen.value, (open) => {
|
||||
if (open) {
|
||||
newOncostPct.value = coopStore.payrollOncostPct || 25;
|
||||
newMinCashThreshold.value = coopStore.minCashThreshold || 0;
|
||||
}
|
||||
});
|
||||
|
||||
function resetToDefaults() {
|
||||
newOncostPct.value = 25;
|
||||
newMinCashThreshold.value = 0;
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
// Update payroll oncost percentage
|
||||
coopStore.payrollOncostPct = newOncostPct.value;
|
||||
|
||||
// Update minimum cash threshold
|
||||
coopStore.setMinCashThreshold(newMinCashThreshold.value);
|
||||
|
||||
// Close modal
|
||||
isOpen.value = false;
|
||||
|
||||
// Emit event to parent to refresh calculations
|
||||
emit("settings-updated");
|
||||
}
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue