refactor: update routing paths in app.vue, enhance AnnualBudget component layout, and streamline dashboard and budget pages for improved user experience
This commit is contained in:
parent
09d8794d72
commit
864a81065c
23 changed files with 3211 additions and 1978 deletions
|
|
@ -1,32 +1,166 @@
|
|||
<template>
|
||||
<div class="max-w-6xl mx-auto px-4 py-6 space-y-8" data-ui="dashboard_v1">
|
||||
<section class="py-8 space-y-6 max-w-4xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-semibold">Dashboard</h1>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm text-gray-600">Min</span>
|
||||
<UToggle
|
||||
:model-value="operatingMode === 'target'"
|
||||
@update:model-value="(value) => setOperatingMode(value ? 'target' : 'min')"
|
||||
/>
|
||||
<span class="text-sm text-gray-600">Target</span>
|
||||
<h2 class="text-2xl font-semibold">Compensation</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm">Mode:</span>
|
||||
<button
|
||||
@click="setOperatingMode('min')"
|
||||
class="px-3 py-1 text-sm font-bold border-2 border-black"
|
||||
:class="coopStore.operatingMode === 'min' ? 'bg-black text-white' : 'bg-white'">
|
||||
MIN
|
||||
</button>
|
||||
<button
|
||||
@click="setOperatingMode('target')"
|
||||
class="px-3 py-1 text-sm font-bold border-2 border-black"
|
||||
:class="coopStore.operatingMode === 'target' ? 'bg-black text-white' : 'bg-white'">
|
||||
TARGET
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Core Metrics -->
|
||||
<DashboardCoreMetrics />
|
||||
<!-- Simple Policy Display -->
|
||||
<div class="border-2 border-black bg-white p-4">
|
||||
<div class="text-lg font-bold mb-2">
|
||||
{{ getPolicyName() }} Policy
|
||||
</div>
|
||||
<div class="text-2xl font-mono">
|
||||
{{ getPolicyFormula() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Member Coverage -->
|
||||
<MemberCoveragePanel />
|
||||
<!-- Member List -->
|
||||
<div class="border-2 border-black bg-white">
|
||||
<div class="border-b-2 border-black p-4">
|
||||
<h3 class="font-bold">Members ({{ coopStore.members.length }})</h3>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-300">
|
||||
<div v-if="coopStore.members.length === 0" class="p-4 text-gray-500 text-center">
|
||||
No members yet. Add members in Setup Wizard.
|
||||
</div>
|
||||
<div v-for="member in membersWithPay" :key="member.id" class="p-4 flex justify-between items-center">
|
||||
<div>
|
||||
<div class="font-bold">{{ member.name || 'Unnamed' }}</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
<span v-if="coopStore.policy?.relationship === 'needs-weighted'">
|
||||
Needs: {{ $format.currency(member.minMonthlyNeeds || 0) }}/month
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ member.hoursPerMonth || 0 }} hrs/month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="font-mono font-bold">{{ $format.currency(member.expectedPay) }}</div>
|
||||
<div class="text-xs" :class="member.coverage >= 100 ? 'text-green-600' : 'text-red-600'">
|
||||
{{ member.coverage }}% covered
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- Total -->
|
||||
<div class="border-2 border-black bg-gray-100 p-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-bold">Total Monthly Payroll</span>
|
||||
<span class="text-xl font-mono font-bold">{{ $format.currency(totalPayroll) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mt-2 text-sm text-gray-600">
|
||||
<span>+ Oncosts ({{ coopStore.payrollOncostPct }}%)</span>
|
||||
<span class="font-mono">{{ $format.currency(totalPayroll * coopStore.payrollOncostPct / 100) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mt-2 pt-2 border-t border-gray-400">
|
||||
<span class="font-bold">Total Cost</span>
|
||||
<span class="text-xl font-mono font-bold">{{ $format.currency(totalPayroll * (1 + coopStore.payrollOncostPct / 100)) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="navigateTo('/coop-builder')"
|
||||
class="px-4 py-2 border-2 border-black bg-white font-bold hover:bg-gray-100">
|
||||
Edit in Setup Wizard
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Import components explicitly to avoid auto-import issues
|
||||
import DashboardCoreMetrics from '~/components/dashboard/DashboardCoreMetrics.vue'
|
||||
import MemberCoveragePanel from '~/components/dashboard/MemberCoveragePanel.vue'
|
||||
const { $format } = useNuxtApp();
|
||||
const coopStore = useCoopBuilderStore();
|
||||
|
||||
// Access composable data
|
||||
const { operatingMode, setOperatingMode } = useCoopBuilder()
|
||||
// Calculate member pay based on policy
|
||||
const membersWithPay = computed(() => {
|
||||
const policyType = coopStore.policy?.relationship || 'equal-pay';
|
||||
const totalHours = coopStore.members.reduce((sum, m) => sum + (m.hoursPerMonth || 0), 0);
|
||||
const totalNeeds = coopStore.members.reduce((sum, m) => sum + (m.minMonthlyNeeds || 0), 0);
|
||||
|
||||
return coopStore.members.map(member => {
|
||||
let expectedPay = 0;
|
||||
const hours = member.hoursPerMonth || 0;
|
||||
|
||||
if (policyType === 'equal-pay') {
|
||||
// Equal pay: hours × wage
|
||||
expectedPay = hours * coopStore.equalHourlyWage;
|
||||
} else if (policyType === 'hours-weighted') {
|
||||
// Hours weighted: proportion of total hours
|
||||
expectedPay = totalHours > 0 ? (hours / totalHours) * (totalHours * coopStore.equalHourlyWage) : 0;
|
||||
} else if (policyType === 'needs-weighted') {
|
||||
// Needs weighted: based on individual needs
|
||||
const needs = member.minMonthlyNeeds || 0;
|
||||
expectedPay = totalNeeds > 0 ? (needs / totalNeeds) * (totalHours * coopStore.equalHourlyWage) : 0;
|
||||
}
|
||||
|
||||
const actualPay = member.monthlyPayPlanned || expectedPay;
|
||||
const coverage = expectedPay > 0 ? Math.round((actualPay / expectedPay) * 100) : 100;
|
||||
|
||||
return {
|
||||
...member,
|
||||
expectedPay,
|
||||
actualPay,
|
||||
coverage
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// Total payroll
|
||||
const totalPayroll = computed(() => {
|
||||
return membersWithPay.value.reduce((sum, m) => sum + m.expectedPay, 0);
|
||||
});
|
||||
|
||||
// Operating mode toggle
|
||||
function setOperatingMode(mode: 'min' | 'target') {
|
||||
coopStore.setOperatingMode(mode);
|
||||
}
|
||||
|
||||
// Get current policy name
|
||||
function getPolicyName() {
|
||||
// Check both coopStore.policy and the root level policy.relationship
|
||||
const policyType = coopStore.policy?.relationship || coopStore.policy || 'equal-pay';
|
||||
|
||||
if (policyType === 'equal-pay') return 'Equal Pay';
|
||||
if (policyType === 'hours-weighted') return 'Hours Based';
|
||||
if (policyType === 'needs-weighted') return 'Needs Based';
|
||||
return 'Equal Pay'; // fallback
|
||||
}
|
||||
|
||||
// Get policy formula display
|
||||
function getPolicyFormula() {
|
||||
const policyType = coopStore.policy?.relationship || coopStore.policy || 'equal-pay';
|
||||
const mode = coopStore.operatingMode === 'target' ? 'Target' : 'Min';
|
||||
|
||||
if (policyType === 'equal-pay') {
|
||||
return `${$format.currency(coopStore.equalHourlyWage)}/hour × ${mode} Hours`;
|
||||
}
|
||||
if (policyType === 'hours-weighted') {
|
||||
return `Based on ${mode} Hours Proportion`;
|
||||
}
|
||||
if (policyType === 'needs-weighted') {
|
||||
return `Based on Individual Needs`;
|
||||
}
|
||||
return `${$format.currency(coopStore.equalHourlyWage)}/hour × ${mode} Hours`;
|
||||
}
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue