app/pages/tools/project-budget.vue

130 lines
4.3 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>
<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="/tools/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>