app/pages/coop-builder.vue

442 lines
16 KiB
Vue

<template>
<div>
<!-- No WizardSubnav for co-op setup tool -->
<section class="py-8 max-w-4xl mx-auto font-mono">
<!-- Header -->
<div class="mb-10 text-center">
<h1
class="text-3xl font-black text-black dark:text-white mb-4 leading-tight uppercase tracking-wide border-t-2 border-b-2 border-black dark:border-white py-4">
Budget Builder
</h1>
</div>
<!-- Completed State -->
<div v-if="isCompleted" class="text-center py-12 relative">
<!-- Dithered shadow background -->
<div class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
<div
class="relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-white p-8">
<div
class="w-16 h-16 bg-black dark:bg-white border-2 border-black dark:border-white flex items-center justify-center mx-auto mb-4">
<UIcon
name="i-heroicons-check"
class="w-8 h-8 text-white dark:text-black" />
</div>
<h2
class="text-2xl font-bold text-black dark:text-white mb-2 uppercase tracking-wide">
You're all set!
</h2>
<p class="text-neutral-600 dark:text-neutral-400 mb-6">
Your co-op is configured and ready to go.
</p>
<div class="flex justify-center gap-4">
<button
class="export-btn"
@click="restartWizard"
:disabled="isResetting">
Start Over
</button>
<button class="export-btn primary" @click="navigateTo('/budget')">
Go to Dashboard
</button>
</div>
</div>
</div>
<!-- Vertical Steps Layout -->
<div v-else class="space-y-4">
<!-- Step 1: Pay Policy -->
<div class="relative">
<!-- Dithered shadow for selected state -->
<div
v-if="focusedStep === 1"
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
<div
:class="[
'relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-neutral-500 overflow-hidden',
focusedStep === 1 ? 'item-selected' : '',
]">
<div
class="p-8 cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
@click="setFocusedStep(1)">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="w-8 h-8 flex items-center justify-center text-sm font-bold border-2"
:class="
policiesValid
? 'bg-black dark:bg-white text-white dark:text-black border-black dark:border-white'
: 'bg-white dark:bg-neutral-950 text-black dark:text-white border-black dark:border-white'
">
<UIcon
v-if="policiesValid"
name="i-heroicons-check"
class="w-4 h-4" />
<span v-else>1</span>
</div>
<div>
<h3
class="text-2xl font-black text-black dark:text-white uppercase tracking-wide">
Choose pay approach
</h3>
</div>
</div>
<UIcon
name="i-heroicons-chevron-down"
class="w-6 h-6 text-black dark:text-white transition-transform font-bold"
:class="{ 'rotate-180': focusedStep === 1 }" />
</div>
</div>
<div
v-if="focusedStep === 1"
class="p-8 bg-neutral-50 dark:bg-neutral-900 border-t-2 border-black dark:border-neutral-500">
<WizardPoliciesStep @save-status="handleSaveStatus" />
</div>
</div>
</div>
<!-- Step 2: Members -->
<div class="relative">
<!-- Dithered shadow for selected state -->
<div
v-if="focusedStep === 2"
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
<div
:class="[
'relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-neutral-500 overflow-hidden',
focusedStep === 2 ? 'item-selected' : '',
]">
<div
class="p-8 cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
@click="setFocusedStep(2)">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="w-8 h-8 flex items-center justify-center text-sm font-bold border-2"
:class="
membersValid
? 'bg-black dark:bg-white text-white dark:text-black border-black dark:border-neutral-500'
: 'bg-white dark:bg-neutral-950 text-black dark:text-white border-black dark:border-neutral-500'
">
<UIcon
v-if="membersValid"
name="i-heroicons-check"
class="w-4 h-4" />
<span v-else>2</span>
</div>
<div>
<h3
class="text-2xl font-black text-black dark:text-white uppercase tracking-wide">
Add your team
</h3>
</div>
</div>
<UIcon
name="i-heroicons-chevron-down"
class="w-6 h-6 text-black dark:text-white transition-transform font-bold"
:class="{ 'rotate-180': focusedStep === 2 }" />
</div>
</div>
<div
v-if="focusedStep === 2"
class="p-8 bg-neutral-50 dark:bg-neutral-900 border-t-2 border-black dark:border-neutral-500">
<WizardMembersStep @save-status="handleSaveStatus" />
</div>
</div>
</div>
<!-- Step 3: Costs -->
<div class="relative">
<!-- Dithered shadow for selected state -->
<div
v-if="focusedStep === 3"
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
<div
:class="[
'relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-neutral-500 overflow-hidden',
focusedStep === 3 ? 'item-selected' : '',
]">
<div
class="p-8 cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
@click="setFocusedStep(3)">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="w-8 h-8 flex items-center justify-center text-sm font-bold border-2"
:class="
costsValid
? 'bg-black dark:bg-white text-white dark:text-black border-black dark:border-neutral-500'
: 'bg-white dark:bg-neutral-950 text-black dark:text-white border-black dark:border-neutral-500'
">
<UIcon
v-if="costsValid"
name="i-heroicons-check"
class="w-4 h-4" />
<span v-else>3</span>
</div>
<div>
<h3
class="text-2xl font-black text-black dark:text-white uppercase tracking-wide">
Expenses
</h3>
</div>
</div>
<UIcon
name="i-heroicons-chevron-down"
class="w-6 h-6 text-black dark:text-white transition-transform font-bold"
:class="{ 'rotate-180': focusedStep === 3 }" />
</div>
</div>
<div
v-if="focusedStep === 3"
class="p-8 bg-neutral-50 dark:bg-neutral-900 border-t-2 border-black dark:border-neutral-500">
<WizardCostsStep @save-status="handleSaveStatus" />
</div>
</div>
</div>
<!-- Step 4: Revenue -->
<div class="relative">
<!-- Dithered shadow for selected state -->
<div
v-if="focusedStep === 4"
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
<div
:class="[
'relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-neutral-500 overflow-hidden',
focusedStep === 4 ? 'item-selected' : '',
]">
<div
class="p-8 cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
@click="setFocusedStep(4)">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="w-8 h-8 flex items-center justify-center text-sm font-bold border-2"
:class="
streamsValid
? 'bg-black dark:bg-white text-white dark:text-black border-black dark:border-neutral-500'
: 'bg-white dark:bg-neutral-950 text-black dark:text-white border-black dark:border-neutral-500'
">
<UIcon
v-if="streamsValid"
name="i-heroicons-check"
class="w-4 h-4" />
<span v-else>4</span>
</div>
<div>
<h3
class="text-2xl font-black text-black dark:text-white uppercase tracking-wide">
Revenue streams
</h3>
</div>
</div>
<UIcon
name="i-heroicons-chevron-down"
class="w-6 h-6 text-black dark:text-white transition-transform font-bold"
:class="{ 'rotate-180': focusedStep === 4 }" />
</div>
</div>
<div
v-if="focusedStep === 4"
class="p-8 bg-neutral-50 dark:bg-neutral-900 border-t-2 border-black dark:border-neutral-500">
<WizardRevenueStep @save-status="handleSaveStatus" />
</div>
</div>
</div>
<!-- Progress Actions -->
<div class="flex justify-between items-center pt-8">
<button
class="export-btn"
@click="resetWizard"
:disabled="isResetting">
Clear Data
</button>
<div class="flex items-center gap-4">
<!-- Save status -->
<div
class="flex items-center gap-2 text-sm font-mono uppercase tracking-wide">
<UIcon
v-if="saveStatus === 'saving'"
name="i-heroicons-arrow-path"
class="w-4 h-4 animate-spin text-neutral-500 dark:text-neutral-400" />
<UIcon
v-if="saveStatus === 'saved'"
name="i-heroicons-check-circle"
class="w-4 h-4 text-black dark:text-white" />
<span
v-if="saveStatus === 'saving'"
class="text-neutral-500 dark:text-neutral-400"
>Saving...</span
>
<span
v-if="saveStatus === 'saved'"
class="text-black dark:text-white"
>Saved</span
>
</div>
<UTooltip :text="incompleteSectionsText" :prevent="canComplete">
<button
class="export-btn primary"
:class="{ 'opacity-50 cursor-not-allowed': !canComplete }"
:disabled="!canComplete"
@click="canComplete ? completeWizard() : null">
Complete Setup
</button>
</UTooltip>
</div>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
// Store
const coop = useCoopBuilder();
// UI state
const focusedStep = ref(1);
const saveStatus = ref("");
const isResetting = ref(false);
const isCompleted = ref(false);
// Local validity flags for step headers
const membersValid = computed(() => {
// Valid if at least one member with a name and positive hours
return coop.members.value.some((m: any) => {
const hasName = typeof m.name === "string" && m.name.trim().length > 0;
const hours = Number((m as any).hoursPerMonth ?? 0);
return hasName && Number.isFinite(hours) && hours > 0;
});
});
const policiesValid = computed(() => {
// Placeholder policy validity; mark true when wage text or policy set exists
// Since policy not persisted yet in this store, consider valid when any member exists
return coop.members.value.length > 0;
});
const costsValid = computed(() => {
// Costs are optional, so always mark as valid for now
return true;
});
const streamsValid = computed(() => {
// Valid if all streams have name, category, and non-negative monthly
return (
coop.streams.value.length > 0 &&
coop.streams.value.every((s: any) => {
const monthly = Number((s as any).monthly ?? 0);
return (s.label || "").toString().trim().length > 0 && monthly >= 0;
})
);
});
// Check if we have basic data for scenario exploration
const hasBasicData = computed(() => {
return membersValid.value && (costsValid.value || streamsValid.value);
});
// Computed validation - all 4 steps must be valid
const canComplete = computed(() => {
return (
policiesValid.value &&
membersValid.value &&
costsValid.value &&
streamsValid.value
);
});
// Generate tooltip text for incomplete sections
const incompleteSectionsText = computed(() => {
if (canComplete.value) return "";
const incomplete = [];
if (!policiesValid.value) incomplete.push("Choose pay approach");
if (!membersValid.value) incomplete.push("Add team members");
if (!costsValid.value) incomplete.push("Add monthly costs");
if (!streamsValid.value) incomplete.push("Add revenue streams");
return `Complete these sections: ${incomplete.join(", ")}`;
});
// Save status handler
function handleSaveStatus(status: "saving" | "saved" | "error") {
saveStatus.value = status;
if (status === "saved") {
// Clear status after delay
setTimeout(() => {
if (saveStatus.value === "saved") {
saveStatus.value = "";
}
}, 2000);
}
}
// Step management
function setFocusedStep(step: number) {
console.log("Setting focused step to:", step, "Current:", focusedStep.value);
// Toggle if clicking on already focused step
if (focusedStep.value === step) {
focusedStep.value = 0; // Close the section
} else {
focusedStep.value = step; // Open the section
}
console.log("Focused step is now:", focusedStep.value);
}
function completeWizard() {
// Mark setup as complete and show restart button for testing
isCompleted.value = true;
}
async function resetWizard() {
isResetting.value = true;
// Reset centralized store
coop.reset();
saveStatus.value = "";
// Small delay for UX
await new Promise((resolve) => setTimeout(resolve, 300));
isResetting.value = false;
}
async function restartWizard() {
isResetting.value = true;
// Reset completion state
isCompleted.value = false;
focusedStep.value = 1;
// Reset centralized store
coop.reset();
saveStatus.value = "";
// Small delay for UX
await new Promise((resolve) => setTimeout(resolve, 300));
isResetting.value = false;
}
// SEO
useSeoMeta({
title: "Budget Builder",
description:
"Build your co-op's financial foundation: set up members, policies, costs, and revenue streams.",
});
</script>