215 lines
6.4 KiB
Vue
215 lines
6.4 KiB
Vue
<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>
|
|
</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>
|
|
|
|
<!-- 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>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const emit = defineEmits<{
|
|
"save-status": [status: "saving" | "saved" | "error"];
|
|
}>();
|
|
|
|
// Store
|
|
const coop = useCoopBuilder();
|
|
const store = useCoopBuilderStore();
|
|
|
|
// Initialize from store
|
|
const selectedPolicy = ref(coop.policy.value?.relationship || "equal-pay");
|
|
const roleBands = ref(coop.policy.value?.roleBands || {});
|
|
const wageText = ref(String(store.equalHourlyWage || ""));
|
|
|
|
function parseNumberInput(val: unknown): number {
|
|
if (typeof val === "number") return val;
|
|
if (typeof val === "string") {
|
|
const cleaned = val.replace(/,/g, ".").replace(/[^0-9.\-]/g, "");
|
|
const parsed = parseFloat(cleaned);
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// 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",
|
|
},
|
|
{
|
|
value: "hours-weighted",
|
|
label: "Hours-weighted - Allocate based on hours worked",
|
|
},
|
|
{ value: "role-banded", label: "Role-banded - Different amounts per role" },
|
|
];
|
|
|
|
// 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 }));
|
|
});
|
|
|
|
function updatePolicy(value: string) {
|
|
selectedPolicy.value = value;
|
|
coop.setPolicy(
|
|
value as "equal-pay" | "needs-weighted" | "hours-weighted" | "role-banded"
|
|
);
|
|
|
|
// Trigger payroll reallocation after policy change
|
|
const allocatedMembers = coop.allocatePayroll();
|
|
allocatedMembers.forEach((m) => {
|
|
coop.upsertMember(m);
|
|
});
|
|
|
|
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");
|
|
}
|
|
|
|
// Text input for wage with validation (initialized above)
|
|
|
|
function validateAndSaveWage(value: string) {
|
|
const cleanValue = value.replace(/[^\d.]/g, "");
|
|
const numValue = parseFloat(cleanValue);
|
|
|
|
wageText.value = cleanValue;
|
|
|
|
if (!isNaN(numValue) && numValue >= 0) {
|
|
coop.setEqualWage(numValue);
|
|
|
|
// Trigger payroll reallocation after wage change
|
|
const allocatedMembers = coop.allocatePayroll();
|
|
allocatedMembers.forEach((m) => {
|
|
coop.upsertMember(m);
|
|
});
|
|
|
|
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>
|