app/components/WizardMembersStep.vue

232 lines
7.2 KiB
Vue

<template>
<div class="max-w-4xl mx-auto space-y-6">
<!-- Section Header -->
<div class="mb-8">
<h3 class="text-2xl font-black text-black dark:text-white mb-2">
Who's on your team?
</h3>
<p class="text-neutral-600 dark:text-neutral-200">
Add everyone who'll be working in the co-op. Based on your pay approach,
we'll collect the right information for each person.
</p>
<!-- Debug info -->
<div class="mt-2 p-2 bg-neutral-100 dark:bg-neutral-800 rounded text-xs">
Debug: Policy = {{ currentPolicy }}, Needs field shown =
{{ isNeedsWeighted }}
</div>
</div>
<!-- Members List -->
<div class="space-y-3">
<div
v-if="members.length === 0"
class="text-center py-12 border-2 border-dashed border-neutral-300 dark:border-neutral-600 rounded-xl bg-white dark:bg-neutral-950 shadow-sm">
<h4 class="font-medium text-neutral-900 dark:text-neutral-100 mb-2">
No team members yet
</h4>
<p class="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
Get started by adding your first team member.
</p>
<UButton @click="addMember" size="lg" variant="solid" color="primary">
<UIcon name="i-heroicons-plus" class="mr-2" />
Add your first member
</UButton>
</div>
<div
v-for="(member, index) in members"
:key="member.id"
class="p-6 border-2 border-black dark:border-neutral-400 rounded-xl bg-white dark:bg-neutral-950 shadow-md">
<!-- Header row with name and optional coverage chip -->
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-4 flex-1">
<UInput
v-model="member.displayName"
placeholder="Member name"
size="xl"
class="text-xl w-full font-bold flex-1"
@update:model-value="saveMember(member)"
@blur="saveMember(member)" />
</div>
<UButton
size="xs"
variant="solid"
color="error"
class="ml-4"
@click="removeMember(member.id)"
:ui="{
base: 'cursor-pointer hover:opacity-90 transition-opacity',
}">
<UIcon name="i-heroicons-trash" class="w-4 h-4" />
</UButton>
</div>
<!-- Essential fields based on policy -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<UFormField label="Hours per month" required>
<UInputNumber
v-model="member.capacity.targetHours"
:min="0"
:max="500"
:step="1"
placeholder="160"
size="md"
class="text-sm font-medium w-full"
@update:model-value="saveMember(member)" />
</UFormField>
<!-- Show minimum needs field when needs-weighted policy is selected -->
<UFormField
v-if="isNeedsWeighted"
:label="`Minimum needs (${getCurrencySymbol(
coop.currency.value
)}/month)`"
required>
<UInputNumber
v-model="member.minMonthlyNeeds"
:min="0"
:max="50000"
:step="10"
placeholder="2500"
size="md"
class="text-sm font-medium w-full"
@update:model-value="saveMember(member)" />
</UFormField>
</div>
</div>
<!-- Add Member -->
<div v-if="members.length > 0" class="flex justify-center">
<UButton
@click="addMember"
size="lg"
variant="solid"
:ui="{
base: 'cursor-pointer hover:scale-105 transition-transform',
leadingIcon: 'hover:rotate-90 transition-transform',
}">
<UIcon name="i-heroicons-plus" class="mr-2" />
Add another member
</UButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useDebounceFn } from "@vueuse/core";
import { coverage } from "~/types/members";
import { getCurrencySymbol } from "~/utils/currency";
const emit = defineEmits<{
"save-status": [status: "saving" | "saved" | "error"];
}>();
// Store
const coop = useCoopBuilder();
const members = computed(() =>
coop.members.value.map((m) => ({
// Map store fields to component expectations
id: m.id,
displayName: m.name,
capacity: {
targetHours: Number(m.hoursPerMonth) || 0,
},
payRelationship: "FullyPaid", // Default since not in store yet
minMonthlyNeeds: Number(m.minMonthlyNeeds) || 0,
monthlyPayPlanned: Number(m.monthlyPayPlanned) || 0,
}))
);
// Get current policy to determine which fields to show
const isNeedsWeighted = computed(() => {
const policy = coop.policy.value?.relationship;
return policy === "needs-weighted";
});
// Also expose policy for debugging in template
const currentPolicy = computed(() => coop.policy.value?.relationship || "none");
// Simplified options - removed pay relationship as it's now in the policies step
const payRelationshipOptions = [
{ label: "Fully Paid", value: "FullyPaid" },
{ label: "Hybrid", value: "Hybrid" },
{ label: "Supplemental", value: "Supplemental" },
{ label: "Volunteer/Deferred", value: "VolunteerOrDeferred" },
];
const riskBandOptions = [
{ label: "Low Risk", value: "Low" },
{ label: "Medium Risk", value: "Medium" },
{ label: "High Risk", value: "High" },
];
// Computeds
const totalTargetHours = computed(() =>
members.value.reduce((sum, m) => sum + (m.capacity?.targetHours || 0), 0)
);
const avgExternalCoverage = computed(() => {
if (members.value.length === 0) return 0;
const total = members.value.reduce(
(sum, m) => sum + (m.externalCoveragePct || 0),
0
);
return Math.round(total / members.value.length);
});
// Live-write with debounce
const debouncedSave = useDebounceFn((member: any) => {
emit("save-status", "saving");
try {
// Convert component format back to store format
const memberData = {
id: member.id,
name: member.displayName || "",
hoursPerMonth: Number(member.capacity?.targetHours) || 0,
minMonthlyNeeds: Number(member.minMonthlyNeeds) || 0,
monthlyPayPlanned: Number(member.monthlyPayPlanned) || 0,
};
coop.upsertMember(memberData);
emit("save-status", "saved");
} catch (error) {
console.error("Failed to save member:", error);
emit("save-status", "error");
}
}, 300);
function saveMember(member: any) {
debouncedSave(member);
}
// Validation functions (simplified since UInputNumber handles numeric validation)
function validateAndSavePercentage(value: string, member: any) {
const numValue = parseFloat(value.replace(/[^\d.]/g, ""));
member.externalCoveragePct = isNaN(numValue)
? 0
: Math.min(100, Math.max(0, numValue));
saveMember(member);
}
function memberCoverage(member: any) {
return coverage(member.minMonthlyNeeds || 0, member.monthlyPayPlanned || 0);
}
function addMember() {
const newMember = {
id: Date.now().toString(),
name: "",
hoursPerMonth: 0,
minMonthlyNeeds: 0,
monthlyPayPlanned: 0,
};
coop.upsertMember(newMember);
}
function removeMember(id: string) {
coop.removeMember(id);
}
</script>