app/pages/dashboard.vue

166 lines
No EOL
6.1 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 max-w-4xl mx-auto">
<!-- Header -->
<div class="flex items-center justify-between">
<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>
<!-- 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 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>
<!-- 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">
const { $format } = useNuxtApp();
const coopStore = useCoopBuilderStore();
// 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>