refactor: remove deprecated components and streamline member coverage calculations, enhance budget management with improved payroll handling, and update UI elements for better clarity

This commit is contained in:
Jennie Robinson Faber 2025-09-06 09:48:57 +01:00
parent 983aeca2dc
commit 09d8794d72
42 changed files with 2166 additions and 2974 deletions

View file

@ -1,97 +1,78 @@
<template>
<div class="max-w-4xl mx-auto space-y-6">
<!-- Section Header with Export Controls -->
<div class="flex items-center justify-between mb-8">
<div>
<h3 class="text-2xl font-black text-black mb-2">Set your wage & pay policy</h3>
<p class="text-neutral-600">
Choose how to allocate payroll among members and set the base hourly rate.
</p>
</div>
<div class="flex items-center gap-3">
<UButton variant="outline" color="neutral" size="sm" @click="exportPolicies">
<UIcon name="i-heroicons-arrow-down-tray" class="mr-1" />
Export
</UButton>
</div>
<!-- Section Header -->
<div class="mb-8">
<h3 class="text-2xl font-black text-black mb-2">
How will you share money?
</h3>
<p class="text-neutral-600">
This is the foundation of your co-op's finances. Choose a pay approach
and set your hourly rate.
</p>
</div>
<!-- Pay Policy Selection -->
<div class="p-6 border-3 border-black rounded-xl bg-white shadow-md">
<h4 class="font-bold mb-4">Pay Allocation Policy</h4>
<div class="space-y-3">
<label
v-for="option in policyOptions"
:key="option.value"
class="flex items-start gap-3 cursor-pointer hover:bg-gray-50 p-2 rounded-lg transition-colors"
>
<input
type="radio"
:value="option.value"
v-model="selectedPolicy"
@change="updatePolicy(option.value)"
class="mt-1 w-4 h-4 text-black border-2 border-gray-300 focus:ring-2 focus:ring-black"
/>
<span class="text-sm flex-1">{{ option.label }}</span>
</label>
</div>
<!-- Role bands editor if role-banded is selected -->
<div v-if="selectedPolicy === 'role-banded'" class="mt-4 p-4 bg-gray-50 rounded-lg">
<h5 class="text-sm font-medium mb-3">Role Bands (monthly or weight)</h5>
<div class="space-y-2">
<div
v-for="member in uniqueRoles"
:key="member.role"
class="flex items-center gap-2"
>
<span class="text-sm w-32">{{ member.role || "No role" }}</span>
<UInput
v-model="roleBands[member.role || '']"
type="text"
placeholder="3000"
size="sm"
class="w-24"
@update:model-value="updateRoleBands"
/>
</div>
</div>
</div>
<UAlert
class="mt-4"
color="primary"
variant="soft"
icon="i-heroicons-information-circle"
>
<template #description>
Policies affect payroll allocation and member coverage. You can iterate later.
</template>
</UAlert>
<div class="p-6 border-2 border-black rounded-xl bg-white shadow-md">
<h4 class="font-bold mb-2">Step 1: Choose your pay approach</h4>
<p class="text-sm text-gray-600 mb-4">
How should available money be shared among members?
</p>
<URadioGroup
v-model="selectedPolicy"
:items="policyOptions"
@update:model-value="updatePolicy"
variant="list"
size="xl"
class="flex flex-col gap-2 w-full" />
</div>
<!-- Hourly Wage Input -->
<div class="p-6 border-3 border-black rounded-xl bg-white shadow-md">
<h4 class="font-bold mb-4">Base Hourly Wage</h4>
<div class="max-w-md">
<UInput
v-model="wageText"
type="text"
placeholder="0.00"
size="xl"
class="text-4xl font-black w-full h-20"
@update:model-value="validateAndSaveWage"
>
<template #leading>
<span class="text-neutral-500 text-3xl"></span>
</template>
</UInput>
<div class="p-6 border-2 border-black rounded-xl bg-white shadow-md">
<h4 class="font-bold mb-2">Step 2: Set your base wage</h4>
<p class="text-sm text-gray-600 mb-4">
This hourly rate applies to all paid work in your co-op
</p>
<div class="flex gap-4 items-start">
<!-- Currency Selection -->
<UFormField label="Currency" class="w-1/2">
<USelect
v-model="selectedCurrency"
:items="currencySelectOptions"
placeholder="Select currency"
size="xl"
class="w-full"
@update:model-value="updateCurrency">
<template #leading>
<span class="text-lg">{{
getCurrencySymbol(selectedCurrency)
}}</span>
</template>
</USelect>
</UFormField>
<UFormField label="Hourly Rate" class="w-1/2">
<UInput
v-model="wageText"
type="text"
placeholder="0.00"
size="xl"
class="text-2xl font-bold w-full"
@update:model-value="validateAndSaveWage">
<template #leading>
<span class="text-neutral-500 text-xl">{{
getCurrencySymbol(selectedCurrency)
}}</span>
</template>
</UInput>
</UFormField>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { currencyOptions, getCurrencySymbol } from "~/utils/currency";
const emit = defineEmits<{
"save-status": [status: "saving" | "saved" | "error"];
}>();
@ -104,6 +85,7 @@ const store = useCoopBuilderStore();
const selectedPolicy = ref(coop.policy.value?.relationship || "equal-pay");
const roleBands = ref(coop.policy.value?.roleBands || {});
const wageText = ref(String(store.equalHourlyWage || ""));
const selectedCurrency = ref(coop.currency.value || "EUR");
function parseNumberInput(val: unknown): number {
if (typeof val === "number") return val;
@ -115,35 +97,42 @@ function parseNumberInput(val: unknown): number {
return 0;
}
// Pay policy options
// Simplified pay policy options
const policyOptions = [
{
value: "equal-pay",
label: "Equal pay - Everyone gets the same monthly amount",
},
{
value: "needs-weighted",
label: "Needs-weighted - Allocate based on minimum needs",
label: "Equal pay - Everyone gets the same amount",
},
{
value: "hours-weighted",
label: "Hours-weighted - Allocate based on hours worked",
label: "Hours-based - Pay proportional to hours worked",
},
{
value: "needs-weighted",
label: "Needs-based - Pay proportional to individual needs",
},
{ value: "role-banded", label: "Role-banded - Different amounts per role" },
];
// Currency options for USelect (simplified format)
const currencySelectOptions = computed(() =>
currencyOptions.map((currency) => ({
label: `${currency.name} (${currency.code})`,
value: currency.code,
}))
);
// Already initialized above with store values
const uniqueRoles = computed(() => {
const roles = new Set(coop.members.value.map((m) => m.role || ""));
return Array.from(roles).map((role) => ({ role }));
});
// Removed uniqueRoles computed - no longer needed with simplified policies
function updateCurrency(value: string) {
selectedCurrency.value = value;
coop.setCurrency(value);
emit("save-status", "saved");
}
function updatePolicy(value: string) {
selectedPolicy.value = value;
coop.setPolicy(
value as "equal-pay" | "needs-weighted" | "hours-weighted" | "role-banded"
);
coop.setPolicy(value as "equal-pay" | "needs-weighted" | "hours-weighted");
// Trigger payroll reallocation after policy change
const allocatedMembers = coop.allocatePayroll();
@ -154,19 +143,7 @@ function updatePolicy(value: string) {
emit("save-status", "saved");
}
function updateRoleBands() {
coop.setRoleBands(roleBands.value);
// Trigger payroll reallocation after role bands change
if (selectedPolicy.value === "role-banded") {
const allocatedMembers = coop.allocatePayroll();
allocatedMembers.forEach((m) => {
coop.upsertMember(m);
});
}
emit("save-status", "saved");
}
// Removed updateRoleBands - no longer needed with simplified policies
// Text input for wage with validation (initialized above)
@ -188,28 +165,4 @@ function validateAndSaveWage(value: string) {
emit("save-status", "saved");
}
}
function exportPolicies() {
const exportData = {
policies: {
selectedPolicy: coop.policy.value?.relationship || selectedPolicy.value,
roleBands: coop.policy.value?.roleBands || roleBands.value,
equalHourlyWage: store.equalHourlyWage || parseFloat(wageText.value),
},
exportedAt: new Date().toISOString(),
section: "policies",
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `coop-policies-${new Date().toISOString().split("T")[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
</script>