130 lines
4.3 KiB
Vue
130 lines
4.3 KiB
Vue
<template>
|
||
<div class="space-y-8">
|
||
<div class="">
|
||
<h1 class="font-bold text-2xl mb-4">Project Budget Estimate</h1>
|
||
<p class="text-neutral-600 dark:text-neutral-400 mx-auto mb-4">
|
||
This tool provides a rough estimate of what it would cost to build your
|
||
project using the pay policy you've set in the setup.
|
||
</p>
|
||
<div class="space-y-4">
|
||
<!-- Sustainable payroll toggle hidden - defaulting to theoretical maximum -->
|
||
<div class="hidden">
|
||
<span class="text-sm font-medium">Sustainable Payroll</span>
|
||
<USwitch v-model="useTheoreticalPayroll" size="md" />
|
||
<span class="text-sm font-medium">Theoretical Maximum</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="membersWithPay.length === 0" class="text-center py-8">
|
||
<p class="text-neutral-600 dark:text-neutral-400 mb-4">
|
||
No team members set up yet.
|
||
</p>
|
||
<NuxtLink
|
||
to="/coop-builder"
|
||
class="px-4 py-2 border-2 border-black dark:border-white bg-white dark:bg-black text-black dark:text-white font-bold hover:bg-neutral-100 dark:hover:bg-neutral-900">
|
||
Set up your team in Setup Wizard
|
||
</NuxtLink>
|
||
</div>
|
||
|
||
<ProjectBudgetEstimate
|
||
v-else
|
||
:members="membersWithPay"
|
||
:oncost-rate="coopStore.payrollOncostPct / 100"
|
||
:payroll-mode="useTheoreticalPayroll ? 'theoretical' : 'sustainable'" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { allocatePayroll as allocatePayrollImpl } from "~/types/members";
|
||
|
||
const coopStore = useCoopBuilderStore();
|
||
const budgetStore = useBudgetStore();
|
||
|
||
// Toggle between sustainable and theoretical payroll modes - defaulting to theoretical maximum
|
||
const useTheoreticalPayroll = ref(true);
|
||
|
||
// Calculate member pay using different logic based on payroll mode
|
||
const membersWithPay = computed(() => {
|
||
// Use the member's desired hours (targetHours if available, otherwise hoursPerMonth)
|
||
const getHoursForMember = (member: any) => {
|
||
return member.capacity?.targetHours || member.hoursPerMonth || 0;
|
||
};
|
||
|
||
let allocatedMembers;
|
||
|
||
if (useTheoreticalPayroll.value) {
|
||
// Theoretical mode: Calculate true theoretical maximum without revenue constraints
|
||
const allMembers = coopStore.members.map((m: any) => ({
|
||
...m,
|
||
displayName: m.name,
|
||
monthlyPayPlanned: m.monthlyPayPlanned || 0,
|
||
minMonthlyNeeds: m.minMonthlyNeeds || 0,
|
||
hoursPerMonth: m.hoursPerMonth || 0,
|
||
}));
|
||
|
||
const payPolicy = {
|
||
relationship: coopStore.policy.relationship || ("equal-pay" as const),
|
||
};
|
||
|
||
// Calculate theoretical maximum budget: total hours × hourly wage
|
||
const totalHours = allMembers.reduce(
|
||
(sum, m) => sum + (m.hoursPerMonth || 0),
|
||
0
|
||
);
|
||
const hourlyWage = coopStore.equalHourlyWage || 0;
|
||
const theoreticalMaxBudget = totalHours * hourlyWage;
|
||
|
||
allocatedMembers = allocatePayrollImpl(
|
||
allMembers,
|
||
payPolicy,
|
||
theoreticalMaxBudget
|
||
);
|
||
} else {
|
||
// Sustainable mode: Use revenue-constrained allocation (current behavior)
|
||
const { allocatePayroll } = useCoopBuilder();
|
||
const sustainableMembers = allocatePayroll();
|
||
|
||
const today = new Date();
|
||
const currentMonthKey = `${today.getFullYear()}-${String(
|
||
today.getMonth() + 1
|
||
).padStart(2, "0")}`;
|
||
|
||
const payrollExpense = budgetStore.budgetWorksheet.expenses.find(
|
||
(item) =>
|
||
item.id === "expense-payroll-base" || item.id === "expense-payroll"
|
||
);
|
||
const actualPayrollBudget =
|
||
payrollExpense?.monthlyValues?.[currentMonthKey] || 0;
|
||
|
||
const theoreticalTotal = sustainableMembers.reduce(
|
||
(sum, m) => sum + (m.monthlyPayPlanned || 0),
|
||
0
|
||
);
|
||
const scaleFactor =
|
||
theoreticalTotal > 0 ? actualPayrollBudget / theoreticalTotal : 0;
|
||
|
||
allocatedMembers = sustainableMembers.map((member) => ({
|
||
...member,
|
||
monthlyPayPlanned: (member.monthlyPayPlanned || 0) * scaleFactor,
|
||
}));
|
||
}
|
||
|
||
return allocatedMembers
|
||
.map((member: any) => {
|
||
const hours = getHoursForMember(member);
|
||
|
||
return {
|
||
name: member.displayName || "Unnamed",
|
||
hoursPerMonth: hours,
|
||
monthlyPay: member.monthlyPayPlanned || 0,
|
||
};
|
||
})
|
||
.filter((m: any) => m.hoursPerMonth > 0); // Only include members with hours
|
||
});
|
||
|
||
// Set page meta
|
||
definePageMeta({
|
||
title: "Project Budget Estimate",
|
||
});
|
||
</script>
|