refactor: replace Wizard with CoopBuilder in navigation, enhance budget store structure, and streamline template components for improved user experience
This commit is contained in:
parent
eede87a273
commit
f67b138d95
33 changed files with 4970 additions and 2451 deletions
652
pages/budget.vue
652
pages/budget.vue
|
|
@ -1,368 +1,320 @@
|
|||
<template>
|
||||
<section class="py-8 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-semibold">Operating Plan</h2>
|
||||
<USelect
|
||||
v-model="selectedMonth"
|
||||
:options="months"
|
||||
placeholder="Select month" />
|
||||
<h2 class="text-2xl font-semibold">Budget Worksheet</h2>
|
||||
<div class="flex items-center gap-4">
|
||||
<UButton @click="forceReinitialize" variant="outline" size="sm" color="orange">Force Re-init</UButton>
|
||||
<UButton @click="resetWorksheet" variant="outline" size="sm">Reset All</UButton>
|
||||
<UButton @click="exportBudget" variant="outline" size="sm">Export</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cash Waterfall Summary -->
|
||||
<!-- Budget Worksheet Table -->
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">
|
||||
Cash Waterfall - {{ selectedMonth }}
|
||||
</h3>
|
||||
</template>
|
||||
<div
|
||||
class="flex items-center justify-between py-4 border-b border-neutral-200">
|
||||
<div class="flex items-center gap-8">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-blue-600">
|
||||
€{{ budgetMetrics.grossRevenue.toLocaleString() }}
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">Gross Revenue</div>
|
||||
</div>
|
||||
<UIcon name="i-heroicons-arrow-right" class="text-neutral-400" />
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-red-600">
|
||||
-€{{ budgetMetrics.totalFees.toLocaleString() }}
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">Fees</div>
|
||||
</div>
|
||||
<UIcon name="i-heroicons-arrow-right" class="text-neutral-400" />
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-green-600">
|
||||
€{{ budgetMetrics.netRevenue.toLocaleString() }}
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">Net Revenue</div>
|
||||
</div>
|
||||
<UIcon name="i-heroicons-arrow-right" class="text-neutral-400" />
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-blue-600">
|
||||
€{{ Math.round(budgetMetrics.savingsAmount).toLocaleString() }}
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">To Savings</div>
|
||||
</div>
|
||||
<UIcon name="i-heroicons-arrow-right" class="text-neutral-400" />
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-purple-600">
|
||||
€{{ Math.round(budgetMetrics.totalPayroll).toLocaleString() }}
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">Payroll</div>
|
||||
</div>
|
||||
<UIcon name="i-heroicons-arrow-right" class="text-neutral-400" />
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-orange-600">
|
||||
€{{ budgetMetrics.totalOverhead.toLocaleString() }}
|
||||
</div>
|
||||
<div class="text-xs text-neutral-600">Overhead</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-lg font-medium">Available for Operations</span>
|
||||
<span class="text-2xl font-bold text-green-600"
|
||||
>€{{
|
||||
Math.round(budgetMetrics.availableForOps).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full border-collapse border border-gray-300 text-sm">
|
||||
<thead>
|
||||
<tr class="bg-gray-50">
|
||||
<th class="border border-gray-300 px-3 py-2 text-left min-w-40 sticky left-0 bg-gray-50 z-10">Category</th>
|
||||
<!-- Monthly columns -->
|
||||
<th v-for="month in monthlyHeaders" :key="month.key" class="border border-gray-300 px-2 py-2 text-center min-w-20">{{ month.label }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Revenue Section -->
|
||||
<tr class="bg-blue-50 font-medium">
|
||||
<td class="border border-gray-300 px-3 py-2 sticky left-0 bg-blue-50 z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Revenue</span>
|
||||
<UButton @click="addRevenueLine" size="xs" variant="soft">+</UButton>
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-gray-300 px-2 py-2" :colspan="monthlyHeaders.length"></td>
|
||||
</tr>
|
||||
|
||||
<!-- Revenue by Category -->
|
||||
<template v-for="(category, categoryName) in budgetStore.groupedRevenue" :key="`revenue-${categoryName}`">
|
||||
<tr v-if="category.length > 0" class="bg-blue-100 font-medium">
|
||||
<td class="border border-gray-300 px-4 py-1 sticky left-0 bg-blue-100 z-10 text-sm text-blue-700">
|
||||
{{ categoryName }} ({{ category.length }} items)
|
||||
</td>
|
||||
<td class="border border-gray-300 px-2 py-1" :colspan="monthlyHeaders.length"></td>
|
||||
</tr>
|
||||
<tr v-for="item in category" :key="item.id">
|
||||
<td class="border border-gray-300 px-3 py-2 sticky left-0 bg-white z-10">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<input
|
||||
v-model="item.name"
|
||||
@blur="saveWorksheet"
|
||||
class="bg-transparent border-none outline-none w-full font-medium"
|
||||
:class="{ 'italic text-gray-500': item.name === 'New Revenue Item' }"
|
||||
/>
|
||||
<UButton @click="removeItem('revenue', item.id)" size="xs" variant="ghost" color="error">×</UButton>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<BudgetCategorySelector
|
||||
v-model="item.subcategory"
|
||||
type="revenue"
|
||||
:main-category="item.mainCategory"
|
||||
placeholder="Subcategory"
|
||||
@update:model-value="saveWorksheet"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Monthly columns -->
|
||||
<td v-for="month in monthlyHeaders" :key="month.key" class="border border-gray-300 px-1 py-1">
|
||||
<input
|
||||
type="number"
|
||||
:value="item.monthlyValues?.[month.key] || 0"
|
||||
@input="updateMonthlyValue('revenue', item.id, month.key, $event.target.value)"
|
||||
class="w-full text-right border-none outline-none bg-transparent"
|
||||
placeholder="0"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Total Revenue Row -->
|
||||
<tr class="bg-blue-100 font-bold">
|
||||
<td class="border border-gray-300 px-3 py-2 sticky left-0 bg-blue-100 z-10">Total Revenue</td>
|
||||
<td v-for="month in monthlyHeaders" :key="month.key" class="border border-gray-300 px-2 py-2 text-right">
|
||||
{{ formatCurrency(budgetStore.monthlyTotals[month.key]?.revenue || 0) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Expenses Section -->
|
||||
<tr class="bg-red-50 font-medium">
|
||||
<td class="border border-gray-300 px-3 py-2 sticky left-0 bg-red-50 z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Expenses</span>
|
||||
<UButton @click="addExpenseLine" size="xs" variant="soft">+</UButton>
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-gray-300 px-2 py-2" :colspan="monthlyHeaders.length"></td>
|
||||
</tr>
|
||||
|
||||
<!-- Expenses by Category -->
|
||||
<template v-for="(category, categoryName) in budgetStore.groupedExpenses" :key="`expense-${categoryName}`">
|
||||
<tr v-if="category.length > 0" class="bg-red-100 font-medium">
|
||||
<td class="border border-gray-300 px-4 py-1 sticky left-0 bg-red-100 z-10 text-sm text-red-700">
|
||||
{{ categoryName }}
|
||||
</td>
|
||||
<td class="border border-gray-300 px-2 py-1" :colspan="monthlyHeaders.length"></td>
|
||||
</tr>
|
||||
<tr v-for="item in category" :key="item.id">
|
||||
<td class="border border-gray-300 px-3 py-2 sticky left-0 bg-white z-10">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<input
|
||||
v-model="item.name"
|
||||
@blur="saveWorksheet"
|
||||
class="bg-transparent border-none outline-none w-full font-medium"
|
||||
:class="{ 'italic text-gray-500': item.name === 'New Expense Item' }"
|
||||
/>
|
||||
<UButton @click="removeItem('expenses', item.id)" size="xs" variant="ghost" color="error">×</UButton>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<BudgetCategorySelector
|
||||
v-model="item.subcategory"
|
||||
type="expenses"
|
||||
:main-category="item.mainCategory"
|
||||
placeholder="Subcategory"
|
||||
@update:model-value="saveWorksheet"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Monthly columns -->
|
||||
<td v-for="month in monthlyHeaders" :key="month.key" class="border border-gray-300 px-1 py-1">
|
||||
<input
|
||||
type="number"
|
||||
:value="item.monthlyValues?.[month.key] || 0"
|
||||
@input="updateMonthlyValue('expenses', item.id, month.key, $event.target.value)"
|
||||
class="w-full text-right border-none outline-none bg-transparent"
|
||||
placeholder="0"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Total Expenses Row -->
|
||||
<tr class="bg-red-100 font-bold">
|
||||
<td class="border border-gray-300 px-3 py-2 sticky left-0 bg-red-100 z-10">Total Expenses</td>
|
||||
<td v-for="month in monthlyHeaders" :key="month.key" class="border border-gray-300 px-2 py-2 text-right">
|
||||
{{ formatCurrency(budgetStore.monthlyTotals[month.key]?.expenses || 0) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Net Income Row -->
|
||||
<tr class="bg-green-100 font-bold text-lg">
|
||||
<td class="border border-gray-300 px-3 py-2 sticky left-0 bg-green-100 z-10">Net Income</td>
|
||||
<td v-for="month in monthlyHeaders" :key="month.key" class="border border-gray-300 px-2 py-2 text-right"
|
||||
:class="getNetIncomeClass(budgetStore.monthlyTotals[month.key]?.net || 0)">
|
||||
{{ formatCurrency(budgetStore.monthlyTotals[month.key]?.net || 0) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<!-- Monthly Revenue Table -->
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">Revenue by Stream</h3>
|
||||
</template>
|
||||
<UTable :rows="revenueStreams" :columns="revenueColumns">
|
||||
<template #name-data="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">{{ row.name }}</span>
|
||||
<RestrictionChip :restriction="row.restrictions" size="xs" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #target-data="{ row }">
|
||||
<span class="font-medium">€{{ row.target.toLocaleString() }}</span>
|
||||
</template>
|
||||
|
||||
<template #committed-data="{ row }">
|
||||
<span class="font-medium text-green-600"
|
||||
>€{{ row.committed.toLocaleString() }}</span
|
||||
>
|
||||
</template>
|
||||
|
||||
<template #actual-data="{ row }">
|
||||
<span
|
||||
class="font-medium"
|
||||
:class="
|
||||
row.actual >= row.committed ? 'text-green-600' : 'text-orange-600'
|
||||
">
|
||||
€{{ row.actual.toLocaleString() }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #variance-data="{ row }">
|
||||
<span :class="row.variance >= 0 ? 'text-green-600' : 'text-red-600'">
|
||||
{{ row.variance >= 0 ? "+" : "" }}€{{
|
||||
row.variance.toLocaleString()
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</UTable>
|
||||
</UCard>
|
||||
|
||||
<!-- Costs Breakdown -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">Costs</h3>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h4 class="font-medium text-sm mb-2">Payroll</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600"
|
||||
>Wages ({{ budgetMetrics.totalHours }}h @ €{{
|
||||
budgetMetrics.hourlyWage
|
||||
}})</span
|
||||
>
|
||||
<span class="font-medium"
|
||||
>€{{
|
||||
Math.round(budgetMetrics.grossWages).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600"
|
||||
>On-costs ({{ budgetMetrics.oncostPct }}%)</span
|
||||
>
|
||||
<span class="font-medium"
|
||||
>€{{
|
||||
Math.round(budgetMetrics.oncosts).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-between text-sm font-medium border-t pt-2">
|
||||
<span>Total Payroll</span>
|
||||
<span
|
||||
>€{{
|
||||
Math.round(budgetMetrics.totalPayroll).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-medium text-sm mb-2">Overhead</h4>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-if="budgetStore.overheadCosts.length === 0"
|
||||
class="text-sm text-neutral-500 italic">
|
||||
No overhead costs added yet
|
||||
</div>
|
||||
<div
|
||||
v-for="cost in budgetStore.overheadCosts"
|
||||
:key="cost.id"
|
||||
class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600">{{ cost.name }}</span>
|
||||
<span class="font-medium"
|
||||
>€{{ (cost.amount || 0).toLocaleString() }}</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-between text-sm font-medium border-t pt-2">
|
||||
<span>Total Overhead</span>
|
||||
<span>€{{ budgetMetrics.totalOverhead.toLocaleString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-medium text-sm mb-2">Production</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600">Dev kits</span>
|
||||
<span class="font-medium">€500</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-between text-sm font-medium border-t pt-2">
|
||||
<span>Total Production</span>
|
||||
<span>€500</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">Net Impact on Savings</h3>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600">Net Revenue</span>
|
||||
<span class="font-medium text-green-600"
|
||||
>€{{ budgetMetrics.netRevenue.toLocaleString() }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600">Total Costs</span>
|
||||
<span class="font-medium text-red-600"
|
||||
>-€{{
|
||||
Math.round(budgetMetrics.totalCosts).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between text-lg font-bold border-t pt-3">
|
||||
<span>Net</span>
|
||||
<span
|
||||
:class="
|
||||
budgetMetrics.monthlyNet >= 0
|
||||
? 'text-green-600'
|
||||
: 'text-red-600'
|
||||
"
|
||||
>{{ budgetMetrics.monthlyNet >= 0 ? "+" : "" }}€{{
|
||||
Math.round(budgetMetrics.monthlyNet).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-neutral-50 rounded-lg p-4">
|
||||
<h4 class="font-medium text-sm mb-3">Allocation</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600">To Savings</span>
|
||||
<span class="font-medium"
|
||||
>€{{
|
||||
Math.round(budgetMetrics.savingsAmount).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-neutral-600">Available</span>
|
||||
<span class="font-medium"
|
||||
>€{{
|
||||
Math.round(
|
||||
budgetMetrics.availableAfterSavings
|
||||
).toLocaleString()
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-neutral-600 space-y-1">
|
||||
<p>
|
||||
<RestrictionChip restriction="Restricted" size="xs" /> funds can
|
||||
only be used for approved purposes.
|
||||
</p>
|
||||
<p>
|
||||
<RestrictionChip restriction="General" size="xs" /> funds have no
|
||||
restrictions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Use real store data
|
||||
const membersStore = useMembersStore();
|
||||
const policiesStore = usePoliciesStore();
|
||||
const streamsStore = useStreamsStore();
|
||||
// Import components explicitly
|
||||
import BudgetCategorySelector from '~/components/BudgetCategorySelector.vue';
|
||||
|
||||
// Use budget worksheet store
|
||||
const budgetStore = useBudgetStore();
|
||||
const cashStore = useCashStore();
|
||||
|
||||
const selectedMonth = ref("2024-01");
|
||||
const months = ref([
|
||||
{ label: "January 2024", value: "2024-01" },
|
||||
{ label: "February 2024", value: "2024-02" },
|
||||
{ label: "March 2024", value: "2024-03" },
|
||||
]);
|
||||
|
||||
// Calculate budget values from real data
|
||||
const budgetMetrics = computed(() => {
|
||||
const totalHours = membersStore.capacityTotals.targetHours || 0;
|
||||
const hourlyWage = policiesStore.equalHourlyWage || 0;
|
||||
const oncostPct = policiesStore.payrollOncostPct || 0;
|
||||
|
||||
const grossWages = totalHours * hourlyWage;
|
||||
const oncosts = grossWages * (oncostPct / 100);
|
||||
const totalPayroll = grossWages + oncosts;
|
||||
|
||||
const totalOverhead = budgetStore.overheadCosts.reduce(
|
||||
(sum, cost) => sum + (cost.amount || 0),
|
||||
0
|
||||
);
|
||||
const grossRevenue = streamsStore.totalMonthlyAmount || 0;
|
||||
|
||||
// Calculate fees from streams with platform fees
|
||||
const totalFees = streamsStore.streams.reduce((sum, stream) => {
|
||||
const revenue = stream.targetMonthlyAmount || 0;
|
||||
const platformFee = (stream.platformFeePct || 0) / 100;
|
||||
const revShareFee = (stream.revenueSharePct || 0) / 100;
|
||||
return sum + revenue * platformFee + revenue * revShareFee;
|
||||
}, 0);
|
||||
|
||||
const netRevenue = grossRevenue - totalFees;
|
||||
const totalCosts = totalPayroll + totalOverhead;
|
||||
const monthlyNet = netRevenue - totalCosts;
|
||||
const savingsAmount = Math.max(0, monthlyNet * 0.3); // Save 30% of positive net if possible
|
||||
const availableAfterSavings = Math.max(0, monthlyNet - savingsAmount);
|
||||
const availableForOps = Math.max(
|
||||
0,
|
||||
netRevenue - totalPayroll - totalOverhead - savingsAmount
|
||||
);
|
||||
|
||||
return {
|
||||
grossRevenue,
|
||||
totalFees,
|
||||
netRevenue,
|
||||
totalCosts,
|
||||
monthlyNet,
|
||||
savingsAmount,
|
||||
availableAfterSavings,
|
||||
totalPayroll,
|
||||
grossWages,
|
||||
oncosts,
|
||||
totalOverhead,
|
||||
availableForOps,
|
||||
totalHours,
|
||||
hourlyWage,
|
||||
oncostPct,
|
||||
};
|
||||
// Generate monthly headers for the next 12 months
|
||||
const monthlyHeaders = computed(() => {
|
||||
const headers = [];
|
||||
const today = new Date();
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const date = new Date(today.getFullYear(), today.getMonth() + i, 1);
|
||||
const monthName = date.toLocaleString('default', { month: 'short' });
|
||||
const year = date.getFullYear();
|
||||
|
||||
headers.push({
|
||||
key: `${year}-${String(date.getMonth() + 1).padStart(2, '0')}`,
|
||||
label: `${monthName} ${year}`
|
||||
});
|
||||
}
|
||||
|
||||
return headers;
|
||||
});
|
||||
|
||||
// Convert streams to budget table format
|
||||
const revenueStreams = computed(() =>
|
||||
streamsStore.streams.map((stream) => ({
|
||||
id: stream.id,
|
||||
name: stream.name,
|
||||
target: stream.targetMonthlyAmount || 0,
|
||||
committed: Math.round((stream.targetMonthlyAmount || 0) * 0.8), // 80% committed assumption
|
||||
actual: Math.round((stream.targetMonthlyAmount || 0) * 0.9), // 90% actual assumption
|
||||
variance: Math.round((stream.targetMonthlyAmount || 0) * 0.1), // 10% positive variance
|
||||
restrictions: stream.restrictions || "General",
|
||||
}))
|
||||
);
|
||||
// Initialize from wizard data on first load
|
||||
onMounted(async () => {
|
||||
console.log('Budget page mounted, initializing...');
|
||||
if (!budgetStore.isInitialized) {
|
||||
await budgetStore.initializeFromWizardData();
|
||||
}
|
||||
console.log('Budget worksheet:', budgetStore.budgetWorksheet);
|
||||
console.log('Grouped revenue:', budgetStore.groupedRevenue);
|
||||
console.log('Grouped expenses:', budgetStore.groupedExpenses);
|
||||
});
|
||||
|
||||
const revenueColumns = [
|
||||
{ id: "name", key: "name", label: "Stream" },
|
||||
{ id: "target", key: "target", label: "Target" },
|
||||
{ id: "committed", key: "committed", label: "Committed" },
|
||||
{ id: "actual", key: "actual", label: "Actual" },
|
||||
{ id: "variance", key: "variance", label: "Variance" },
|
||||
];
|
||||
// Budget worksheet functions
|
||||
function updateValue(category: string, itemId: string, year: string, scenario: string, value: string) {
|
||||
budgetStore.updateBudgetValue(category, itemId, year, scenario, value);
|
||||
}
|
||||
|
||||
function updateMonthlyValue(category: string, itemId: string, monthKey: string, value: string) {
|
||||
budgetStore.updateMonthlyValue(category, itemId, monthKey, value);
|
||||
}
|
||||
|
||||
function addRevenueLine() {
|
||||
console.log('Adding revenue line...');
|
||||
budgetStore.addBudgetItem('revenue', 'New Revenue Item');
|
||||
}
|
||||
|
||||
function addExpenseLine() {
|
||||
console.log('Adding expense line...');
|
||||
budgetStore.addBudgetItem('expenses', 'New Expense Item');
|
||||
}
|
||||
|
||||
function removeItem(category: string, itemId: string) {
|
||||
budgetStore.removeBudgetItem(category, itemId);
|
||||
}
|
||||
|
||||
function saveWorksheet() {
|
||||
// Auto-save is handled by the store persistence
|
||||
console.log('Worksheet saved');
|
||||
}
|
||||
|
||||
function resetWorksheet() {
|
||||
if (confirm('Are you sure you want to reset all budget data? This cannot be undone.')) {
|
||||
budgetStore.resetBudgetWorksheet();
|
||||
// Force re-initialization
|
||||
budgetStore.isInitialized = false;
|
||||
budgetStore.initializeFromWizardData();
|
||||
}
|
||||
}
|
||||
|
||||
async function forceReinitialize() {
|
||||
console.log('Force re-initializing budget...');
|
||||
// Clear all persistent data
|
||||
localStorage.removeItem('urgent-tools-budget');
|
||||
localStorage.removeItem('urgent-tools-streams');
|
||||
localStorage.removeItem('urgent-tools-members');
|
||||
localStorage.removeItem('urgent-tools-policies');
|
||||
|
||||
// Reset the store state completely
|
||||
budgetStore.isInitialized = false;
|
||||
budgetStore.budgetWorksheet.revenue = [];
|
||||
budgetStore.budgetWorksheet.expenses = [];
|
||||
|
||||
// Reset categories to defaults
|
||||
budgetStore.revenueCategories = [
|
||||
'Games & Products',
|
||||
'Services & Contracts',
|
||||
'Grants & Funding',
|
||||
'Community Support',
|
||||
'Partnerships',
|
||||
'Investment Income',
|
||||
'In-Kind Contributions'
|
||||
];
|
||||
|
||||
budgetStore.expenseCategories = [
|
||||
'Salaries & Benefits',
|
||||
'Development Costs',
|
||||
'Equipment & Technology',
|
||||
'Marketing & Outreach',
|
||||
'Office & Operations',
|
||||
'Legal & Professional',
|
||||
'Other Expenses'
|
||||
];
|
||||
|
||||
// Force re-initialization
|
||||
await budgetStore.initializeFromWizardData();
|
||||
|
||||
console.log('Re-initialization complete');
|
||||
}
|
||||
|
||||
function exportBudget() {
|
||||
const data = {
|
||||
worksheet: budgetStore.budgetWorksheet,
|
||||
totals: budgetStore.budgetTotals,
|
||||
exportedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `budget-worksheet-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function formatCurrency(amount: number): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(amount || 0);
|
||||
}
|
||||
|
||||
function getNetIncomeClass(amount: number): string {
|
||||
if (amount > 0) return 'text-green-600';
|
||||
if (amount < 0) return 'text-red-600';
|
||||
return 'text-gray-600';
|
||||
}
|
||||
|
||||
// SEO
|
||||
useSeoMeta({
|
||||
title: "Budget Worksheet - Plan Your Co-op's Financial Future",
|
||||
description: "Interactive budget planning tool with multiple scenarios and multi-year projections for worker cooperatives.",
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
684
pages/coach/skills-to-offers.vue
Normal file
684
pages/coach/skills-to-offers.vue
Normal file
|
|
@ -0,0 +1,684 @@
|
|||
<template>
|
||||
<div class="min-h-screen bg-neutral-50 pb-24">
|
||||
<div class="max-w-4xl mx-auto p-6">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-black mb-2">
|
||||
Turn skills into fair, sellable offers
|
||||
</h1>
|
||||
<p class="text-neutral-600">
|
||||
Tell us what you're good at and who you help. We'll suggest offers that match your co-op's shared capacity.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@click="skipCoach"
|
||||
class="px-4 py-2 text-sm bg-neutral-50 border-2 border-neutral-300 rounded-lg text-neutral-700 hover:bg-neutral-100 hover:border-neutral-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
|
||||
:aria-label="'Skip coach and go to streams tab'"
|
||||
>
|
||||
Skip coach → Streams
|
||||
</button>
|
||||
<button
|
||||
@click="loadSampleData"
|
||||
class="px-4 py-2 text-sm bg-blue-50 border-2 border-blue-200 rounded-lg text-blue-700 hover:bg-blue-100 hover:border-blue-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
|
||||
:aria-label="'Load sample data to see example offers'"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
Load sample data
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section A: Name your strengths -->
|
||||
<section class="mb-8" aria-labelledby="strengths-heading">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<h2 id="strengths-heading" class="text-xl font-bold text-black">
|
||||
A) Name your strengths
|
||||
</h2>
|
||||
<div class="relative group">
|
||||
<button
|
||||
class="w-4 h-4 text-neutral-400 hover:text-neutral-600 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded-full"
|
||||
aria-label="Why limit to 3 skills per member?"
|
||||
>
|
||||
<svg fill="currentColor" viewBox="0 0 20 20" class="w-4 h-4">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-black text-white text-xs rounded-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-10">
|
||||
Focus keeps offers shippable
|
||||
<div class="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-black"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-neutral-600 mb-6">
|
||||
Pick what you can reliably do as a team. We'll keep it simple.
|
||||
</p>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div
|
||||
v-for="member in members"
|
||||
:key="member.id"
|
||||
class="p-6 bg-white border-2 border-neutral-200 rounded-xl shadow-sm"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="font-bold text-black">{{ member.name }}</h3>
|
||||
<p v-if="member.role" class="text-sm text-neutral-600">{{ member.role }}</p>
|
||||
</div>
|
||||
<div class="text-sm text-neutral-500">
|
||||
{{ getSelectedSkillsCount(member.id) }}/3 skills selected
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="skill in availableSkills"
|
||||
:key="skill.id"
|
||||
@click="toggleSkill(member.id, skill.id)"
|
||||
:disabled="!canSelectSkill(member.id, skill.id)"
|
||||
:class="[
|
||||
'px-3 py-1.5 text-sm rounded-full border-2 transition-all duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
|
||||
isSkillSelected(member.id, skill.id)
|
||||
? 'bg-blue-600 text-white border-blue-600 hover:bg-blue-700'
|
||||
: canSelectSkill(member.id, skill.id)
|
||||
? 'bg-white text-neutral-700 border-neutral-300 hover:border-blue-400 hover:text-blue-600'
|
||||
: 'bg-neutral-100 text-neutral-400 border-neutral-200 cursor-not-allowed'
|
||||
]"
|
||||
:aria-pressed="isSkillSelected(member.id, skill.id)"
|
||||
:aria-label="`${isSkillSelected(member.id, skill.id) ? 'Remove' : 'Add'} ${skill.label} skill for ${member.name}`"
|
||||
>
|
||||
{{ skill.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section B: Who do you help? -->
|
||||
<section class="mb-8" aria-labelledby="problems-heading">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<h2 id="problems-heading" class="text-xl font-bold text-black">
|
||||
B) Who do you help?
|
||||
</h2>
|
||||
<div class="relative group">
|
||||
<button
|
||||
class="w-4 h-4 text-neutral-400 hover:text-neutral-600 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded-full"
|
||||
aria-label="Why limit to 2 problem types?"
|
||||
>
|
||||
<svg fill="currentColor" viewBox="0 0 20 20" class="w-4 h-4">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 bg-black text-white text-xs rounded-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-10">
|
||||
Focus keeps offers shippable
|
||||
<div class="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-black"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-neutral-600 mb-6">
|
||||
Choose the problems you can solve this month. We'll suggest time-boxed offers.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<div
|
||||
v-for="problem in availableProblems"
|
||||
:key="problem.id"
|
||||
class="relative"
|
||||
>
|
||||
<button
|
||||
@click="toggleProblem(problem.id)"
|
||||
:disabled="!canSelectProblem(problem.id)"
|
||||
:class="[
|
||||
'px-4 py-2 text-sm rounded-lg border-2 transition-all duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
|
||||
isProblemSelected(problem.id)
|
||||
? 'bg-green-600 text-white border-green-600 hover:bg-green-700'
|
||||
: canSelectProblem(problem.id)
|
||||
? 'bg-white text-neutral-700 border-neutral-300 hover:border-green-400 hover:text-green-600'
|
||||
: 'bg-neutral-100 text-neutral-400 border-neutral-200 cursor-not-allowed'
|
||||
]"
|
||||
:aria-pressed="isProblemSelected(problem.id)"
|
||||
:aria-label="`${isProblemSelected(problem.id) ? 'Remove' : 'Add'} ${problem.label} problem type`"
|
||||
>
|
||||
{{ problem.label }}
|
||||
</button>
|
||||
|
||||
<!-- Examples popover trigger -->
|
||||
<button
|
||||
@click="toggleExamples(problem.id)"
|
||||
@keydown.escape="hideExamples"
|
||||
class="ml-1 text-xs text-neutral-500 hover:text-neutral-700 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded"
|
||||
:aria-label="`See examples for ${problem.label}`"
|
||||
:aria-expanded="showExamples === problem.id"
|
||||
>
|
||||
see examples
|
||||
</button>
|
||||
|
||||
<!-- Examples popover -->
|
||||
<div
|
||||
v-if="showExamples === problem.id"
|
||||
class="absolute z-10 mt-2 p-3 bg-white border-2 border-neutral-200 rounded-lg shadow-lg min-w-64 max-w-sm"
|
||||
role="tooltip"
|
||||
:aria-label="`Examples for ${problem.label}`"
|
||||
>
|
||||
<div class="text-sm">
|
||||
<p class="font-medium text-black mb-2">Examples:</p>
|
||||
<ul class="space-y-1 text-neutral-700">
|
||||
<li v-for="example in problem.examples" :key="example" class="flex items-start">
|
||||
<span class="text-neutral-400 mr-2">•</span>
|
||||
<span>{{ example }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button
|
||||
@click="hideExamples"
|
||||
class="mt-2 text-xs text-blue-600 hover:text-blue-800 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded"
|
||||
aria-label="Close examples"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-neutral-500">
|
||||
{{ selectedProblems.length }}/2 problem types selected
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section C: Suggested offers -->
|
||||
<section class="mb-8" aria-labelledby="offers-heading">
|
||||
<h2 id="offers-heading" class="text-xl font-bold text-black mb-4">
|
||||
C) Suggested offers
|
||||
</h2>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="text-center py-12 bg-white border-2 border-dashed border-blue-200 rounded-xl"
|
||||
>
|
||||
<div class="max-w-md mx-auto">
|
||||
<div class="w-16 h-16 mx-auto mb-4 bg-blue-50 rounded-full flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-blue-500 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="font-medium text-blue-900 mb-2">Generating offers...</h3>
|
||||
<p class="text-blue-700">
|
||||
Creating personalized revenue suggestions based on your selections.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div
|
||||
v-else-if="suggestedOffers.length === 0"
|
||||
class="text-center py-12 bg-white border-2 border-dashed border-neutral-300 rounded-xl"
|
||||
>
|
||||
<div class="max-w-md mx-auto">
|
||||
<div class="w-16 h-16 mx-auto mb-4 bg-neutral-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="font-medium text-neutral-900 mb-2">No offers yet</h3>
|
||||
<p class="text-neutral-600 mb-4">
|
||||
Pick a few skills and a problem—we'll suggest something you can sell this month.
|
||||
</p>
|
||||
<p class="text-sm text-neutral-500">
|
||||
We need at least one shared skill and one problem type to suggest offers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Offer cards -->
|
||||
<div v-else class="grid gap-6 md:grid-cols-2">
|
||||
<div
|
||||
v-for="offer in suggestedOffers"
|
||||
:key="offer.id"
|
||||
class="p-6 bg-white border-2 border-neutral-200 rounded-xl shadow-sm hover:shadow-md transition-shadow"
|
||||
role="article"
|
||||
:aria-label="`Offer: ${offer.name}`"
|
||||
>
|
||||
<h3 class="font-bold text-black mb-3">{{ offer.name }}</h3>
|
||||
|
||||
<!-- Offer chips -->
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs bg-green-50 text-green-700 border border-green-200 rounded-full">
|
||||
Covers ~{{ calculateMonthlyCoverage(offer) }}% of monthly needs at baseline
|
||||
</span>
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs bg-blue-50 text-blue-700 border border-blue-200 rounded-full">
|
||||
Typical payout: {{ getPayoutDaysRange(offer) }}
|
||||
</span>
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs bg-purple-50 text-purple-700 border border-purple-200 rounded-full">
|
||||
Why this
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Scope -->
|
||||
<div class="mb-4">
|
||||
<p class="text-sm font-medium text-neutral-700 mb-2">Scope:</p>
|
||||
<ul class="space-y-1">
|
||||
<li
|
||||
v-for="item in offer.scope"
|
||||
:key="item"
|
||||
class="text-sm text-neutral-600 flex items-start"
|
||||
>
|
||||
<span class="text-neutral-400 mr-2">•</span>
|
||||
<span>{{ item }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Price range -->
|
||||
<div class="mb-4 p-3 bg-neutral-50 rounded-lg">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<span class="text-sm font-medium text-neutral-700">Baseline:</span>
|
||||
<span class="font-bold text-black">${{ offer.price.baseline.toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-sm font-medium text-neutral-700">Stretch:</span>
|
||||
<span class="font-bold text-green-600">${{ offer.price.stretch.toLocaleString() }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500">{{ offer.price.calcNote }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Payout delay -->
|
||||
<div class="mb-4 flex items-center justify-between text-sm">
|
||||
<span class="text-neutral-600">Payment timing:</span>
|
||||
<span class="font-medium text-black">{{ offer.payoutDelayDays }} days</span>
|
||||
</div>
|
||||
|
||||
<!-- Why this works -->
|
||||
<div class="mb-4">
|
||||
<p class="text-sm font-medium text-neutral-700 mb-2">Why this works for your co-op:</p>
|
||||
<ul class="space-y-1">
|
||||
<li
|
||||
v-for="reason in offer.whyThis"
|
||||
:key="reason"
|
||||
class="text-sm text-neutral-600 flex items-start"
|
||||
>
|
||||
<span class="text-green-500 mr-2">✓</span>
|
||||
<span>{{ updateLanguageToCoopTerms(reason) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Risk notes (if any) -->
|
||||
<div v-if="offer.riskNotes.length > 0" class="border-t border-neutral-200 pt-3">
|
||||
<p class="text-sm font-medium text-amber-700 mb-2">Consider:</p>
|
||||
<ul class="space-y-1">
|
||||
<li
|
||||
v-for="risk in offer.riskNotes"
|
||||
:key="risk"
|
||||
class="text-sm text-amber-600 flex items-start"
|
||||
>
|
||||
<span class="text-amber-500 mr-2">⚠</span>
|
||||
<span>{{ risk }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Sticky Footer -->
|
||||
<div class="fixed bottom-0 left-0 right-0 bg-white border-t-2 border-neutral-200 shadow-lg">
|
||||
<div class="max-w-4xl mx-auto p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<button
|
||||
@click="goBack"
|
||||
class="px-4 py-2 text-neutral-700 hover:text-black focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-lg transition-colors"
|
||||
aria-label="Go back to previous page"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@click="regenerateOffers"
|
||||
:disabled="!canRegenerate"
|
||||
:class="[
|
||||
'px-4 py-2 rounded-lg border-2 transition-all duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
|
||||
canRegenerate
|
||||
? 'border-neutral-300 text-neutral-700 hover:border-blue-400 hover:text-blue-600'
|
||||
: 'border-neutral-200 text-neutral-400 cursor-not-allowed'
|
||||
]"
|
||||
:aria-label="canRegenerate ? 'Regenerate offers with current selections' : 'Cannot regenerate - select skills and problems first'"
|
||||
>
|
||||
🔄 Regenerate
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="useOffers"
|
||||
:disabled="suggestedOffers.length === 0"
|
||||
:class="[
|
||||
'px-6 py-2 rounded-lg font-medium transition-all duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
|
||||
suggestedOffers.length > 0
|
||||
? 'bg-blue-600 text-white hover:bg-blue-700'
|
||||
: 'bg-neutral-200 text-neutral-400 cursor-not-allowed'
|
||||
]"
|
||||
:aria-label="suggestedOffers.length > 0 ? 'Add these offers to cover co-op needs' : 'No offers to use - generate offers first'"
|
||||
>
|
||||
Add to plan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Member, SkillTag, ProblemTag, Offer } from "~/types/coaching";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import {
|
||||
membersSample,
|
||||
skillsCatalogSample,
|
||||
problemsCatalogSample,
|
||||
sampleSelections
|
||||
} from "~/sample/skillsToOffersSamples";
|
||||
|
||||
// Store integration
|
||||
const planStore = usePlanStore();
|
||||
|
||||
// Initialize with default data
|
||||
const members = ref<Member[]>([
|
||||
{ id: "1", name: "Alex Chen", role: "Game Designer", hourly: 75, availableHrs: 30 },
|
||||
{ id: "2", name: "Jordan Smith", role: "Developer", hourly: 80, availableHrs: 35 },
|
||||
{ id: "3", name: "Sam Rodriguez", role: "Artist", hourly: 70, availableHrs: 25 }
|
||||
]);
|
||||
|
||||
const availableSkills = ref<SkillTag[]>([
|
||||
{ id: "unity", label: "Unity Development" },
|
||||
{ id: "art", label: "2D/3D Art" },
|
||||
{ id: "design", label: "Game Design" },
|
||||
{ id: "audio", label: "Audio Design" },
|
||||
{ id: "writing", label: "Narrative Writing" },
|
||||
{ id: "marketing", label: "Marketing" },
|
||||
{ id: "business", label: "Business Strategy" },
|
||||
{ id: "web", label: "Web Development" },
|
||||
{ id: "mobile", label: "Mobile Development" },
|
||||
{ id: "consulting", label: "Technical Consulting" }
|
||||
]);
|
||||
|
||||
const availableProblems = ref<ProblemTag[]>([
|
||||
{
|
||||
id: "indie-games",
|
||||
label: "Indie game development",
|
||||
examples: [
|
||||
"Small studios needing extra development capacity",
|
||||
"Solo developers wanting art/audio support",
|
||||
"Teams needing game design consultation"
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "corporate-training",
|
||||
label: "Corporate training games",
|
||||
examples: [
|
||||
"Companies wanting engaging employee training",
|
||||
"HR departments needing onboarding tools",
|
||||
"Safety training for industrial workers"
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "educational",
|
||||
label: "Educational technology",
|
||||
examples: [
|
||||
"Schools needing interactive learning tools",
|
||||
"Universities wanting research simulations",
|
||||
"Non-profits creating awareness campaigns"
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "prototypes",
|
||||
label: "Rapid prototyping",
|
||||
examples: [
|
||||
"Startups validating game concepts",
|
||||
"Publishers testing market fit",
|
||||
"Researchers creating proof-of-concepts"
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
// Set members in store on component mount
|
||||
onMounted(() => {
|
||||
planStore.setMembers(members.value);
|
||||
});
|
||||
|
||||
// Reactive state
|
||||
const selectedSkills = ref<Record<string, string[]>>({});
|
||||
const selectedProblems = ref<string[]>([]);
|
||||
const showExamples = ref<string | null>(null);
|
||||
const offers = ref<Offer[] | null>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
// Use offer suggestor composable
|
||||
const { suggestOffers } = useOfferSuggestor();
|
||||
|
||||
// Catalogs for the suggestor
|
||||
const catalogs = computed(() => ({
|
||||
skills: availableSkills.value,
|
||||
problems: availableProblems.value
|
||||
}));
|
||||
|
||||
// Computed for suggested offers (for backward compatibility)
|
||||
const suggestedOffers = computed(() => offers.value || []);
|
||||
|
||||
// Helper functions for offer chips
|
||||
function calculateMonthlyCoverage(offer: Offer): number {
|
||||
// Estimate monthly burn (simplified calculation)
|
||||
const totalMemberHours = members.value.reduce((sum, m) => sum + m.availableHrs, 0);
|
||||
const avgHourlyRate = members.value.reduce((sum, m) => sum + m.hourly, 0) / members.value.length;
|
||||
const estimatedMonthlyBurn = totalMemberHours * avgHourlyRate * 1.25; // Add on-costs
|
||||
|
||||
return Math.round((offer.price.baseline / estimatedMonthlyBurn) * 100);
|
||||
}
|
||||
|
||||
function getPayoutDaysRange(offer: Offer): string {
|
||||
const days = offer.payoutDelayDays;
|
||||
if (days <= 15) return "0–15 days";
|
||||
if (days <= 30) return "15–30 days";
|
||||
if (days <= 45) return "30–45 days";
|
||||
return `${days} days`;
|
||||
}
|
||||
|
||||
function updateLanguageToCoopTerms(text: string): string {
|
||||
return text
|
||||
.replace(/maximize|maximiz/gi, 'cover needs with')
|
||||
.replace(/optimize|optimiz/gi, 'improve')
|
||||
.replace(/competitive advantage/gi, 'shared capacity')
|
||||
.replace(/market position/gi, 'community standing')
|
||||
.replace(/profit/gi, 'surplus')
|
||||
.replace(/revenue growth/gi, 'sustainable income')
|
||||
.replace(/scale/gi, 'grow together')
|
||||
.replace(/efficiency gains/gi, 'reduce risk')
|
||||
.replace(/leverages/gi, 'uses')
|
||||
.replace(/expertise/gi, 'shared skills')
|
||||
.replace(/builds reputation/gi, 'builds trust in community')
|
||||
.replace(/high-impact/gi, 'meaningful')
|
||||
.replace(/productivity/gi, 'shared capacity');
|
||||
}
|
||||
|
||||
// Sample data loading
|
||||
function loadSampleData() {
|
||||
// Replace data with samples
|
||||
members.value = [...membersSample];
|
||||
availableSkills.value = [...skillsCatalogSample];
|
||||
availableProblems.value = [...problemsCatalogSample];
|
||||
|
||||
// Set pre-selected skills and problems
|
||||
selectedSkills.value = { ...sampleSelections.selectedSkillsByMember };
|
||||
selectedProblems.value = [...sampleSelections.selectedProblems];
|
||||
|
||||
// Update store with new members
|
||||
planStore.setMembers(members.value);
|
||||
|
||||
// Trigger offer generation immediately
|
||||
nextTick(() => {
|
||||
debouncedGenerateOffers();
|
||||
});
|
||||
}
|
||||
|
||||
// Debounced offer generation
|
||||
const debouncedGenerateOffers = useDebounceFn(async () => {
|
||||
const hasSkills = Object.values(selectedSkills.value).some(skills => skills.length > 0);
|
||||
const hasProblems = selectedProblems.value.length > 0;
|
||||
|
||||
if (!hasSkills || !hasProblems) {
|
||||
offers.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const input = {
|
||||
members: members.value,
|
||||
selectedSkillsByMember: selectedSkills.value,
|
||||
selectedProblems: selectedProblems.value
|
||||
};
|
||||
|
||||
const suggestedOffers = suggestOffers(input, catalogs.value);
|
||||
offers.value = suggestedOffers;
|
||||
} catch (error) {
|
||||
console.error('Failed to generate offers:', error);
|
||||
offers.value = null;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// Skill management
|
||||
function toggleSkill(memberId: string, skillId: string) {
|
||||
if (!selectedSkills.value[memberId]) {
|
||||
selectedSkills.value[memberId] = [];
|
||||
}
|
||||
|
||||
const memberSkills = selectedSkills.value[memberId];
|
||||
const index = memberSkills.indexOf(skillId);
|
||||
|
||||
if (index >= 0) {
|
||||
memberSkills.splice(index, 1);
|
||||
} else {
|
||||
memberSkills.push(skillId);
|
||||
}
|
||||
|
||||
debouncedGenerateOffers();
|
||||
}
|
||||
|
||||
function isSkillSelected(memberId: string, skillId: string): boolean {
|
||||
return selectedSkills.value[memberId]?.includes(skillId) || false;
|
||||
}
|
||||
|
||||
function canSelectSkill(memberId: string, skillId: string): boolean {
|
||||
if (isSkillSelected(memberId, skillId)) return true;
|
||||
return getSelectedSkillsCount(memberId) < 3;
|
||||
}
|
||||
|
||||
function getSelectedSkillsCount(memberId: string): number {
|
||||
return selectedSkills.value[memberId]?.length || 0;
|
||||
}
|
||||
|
||||
// Problem management
|
||||
function toggleProblem(problemId: string) {
|
||||
const index = selectedProblems.value.indexOf(problemId);
|
||||
if (index >= 0) {
|
||||
selectedProblems.value.splice(index, 1);
|
||||
} else {
|
||||
selectedProblems.value.push(problemId);
|
||||
}
|
||||
debouncedGenerateOffers();
|
||||
}
|
||||
|
||||
function isProblemSelected(problemId: string): boolean {
|
||||
return selectedProblems.value.includes(problemId);
|
||||
}
|
||||
|
||||
function canSelectProblem(problemId: string): boolean {
|
||||
if (isProblemSelected(problemId)) return true;
|
||||
return selectedProblems.value.length < 2;
|
||||
}
|
||||
|
||||
// Examples popover
|
||||
function toggleExamples(problemId: string) {
|
||||
showExamples.value = showExamples.value === problemId ? null : problemId;
|
||||
}
|
||||
|
||||
function hideExamples() {
|
||||
showExamples.value = null;
|
||||
}
|
||||
|
||||
|
||||
// Footer actions
|
||||
const canRegenerate = computed(() => {
|
||||
const hasSkills = Object.values(selectedSkills.value).some(skills => skills.length > 0);
|
||||
const hasProblems = selectedProblems.value.length > 0;
|
||||
return hasSkills && hasProblems;
|
||||
});
|
||||
|
||||
function goBack() {
|
||||
// Navigate back - would typically use router
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
function regenerateOffers() {
|
||||
if (canRegenerate.value) {
|
||||
// Re-call suggestOffers with same inputs
|
||||
debouncedGenerateOffers();
|
||||
}
|
||||
}
|
||||
|
||||
function useOffers() {
|
||||
if (offers.value && offers.value.length > 0) {
|
||||
// Add offers to plan store as streams
|
||||
planStore.addStreamsFromOffers(offers.value);
|
||||
|
||||
// Navigate back to wizard with success message
|
||||
const router = useRouter();
|
||||
|
||||
// Show success notification
|
||||
console.log(`Added ${offers.value.length} offers as revenue streams to your plan.`);
|
||||
|
||||
// Navigate to wizard revenue step - adjust path as needed for your routing
|
||||
router.push('/wizards'); // This would need to be the correct wizard path
|
||||
|
||||
// Note: The Streams tab activation would be handled by the wizard component
|
||||
// when it detects new streams in the store
|
||||
}
|
||||
}
|
||||
|
||||
function skipCoach() {
|
||||
// Navigate directly to wizard streams without adding offers
|
||||
const router = useRouter();
|
||||
router.push('/wizards'); // Navigate to wizard - streams tab would be activated there
|
||||
}
|
||||
|
||||
// Close examples on click outside
|
||||
onMounted(() => {
|
||||
const handleClickOutside = (event: Event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('[role="tooltip"]') && !target.closest('button[aria-expanded]')) {
|
||||
showExamples.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
468
pages/coop-builder.vue
Normal file
468
pages/coop-builder.vue
Normal file
|
|
@ -0,0 +1,468 @@
|
|||
<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"
|
||||
>
|
||||
Co-op 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: Members -->
|
||||
<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-white 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="
|
||||
membersStore.isValid
|
||||
? '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="membersStore.isValid"
|
||||
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"
|
||||
>
|
||||
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 === 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-white"
|
||||
>
|
||||
<WizardMembersStep @save-status="handleSaveStatus" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Wage -->
|
||||
<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-white 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="
|
||||
policiesStore.isValid
|
||||
? '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="policiesStore.isValid"
|
||||
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"
|
||||
>
|
||||
Set your wage
|
||||
</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-white"
|
||||
>
|
||||
<WizardPoliciesStep @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-white 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 bg-black dark:bg-white text-white dark:text-black border-black dark:border-white"
|
||||
>
|
||||
<UIcon name="i-heroicons-check" class="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
class="text-2xl font-black text-black dark:text-white uppercase tracking-wide"
|
||||
>
|
||||
Monthly costs
|
||||
</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-white"
|
||||
>
|
||||
<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-white 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="
|
||||
streamsStore.hasValidStreams
|
||||
? '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="streamsStore.hasValidStreams"
|
||||
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-white"
|
||||
>
|
||||
<WizardRevenueStep @save-status="handleSaveStatus" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 5: Review -->
|
||||
<div class="relative">
|
||||
<!-- Dithered shadow for selected state -->
|
||||
<div
|
||||
v-if="focusedStep === 5"
|
||||
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 overflow-hidden',
|
||||
focusedStep === 5 ? 'item-selected' : '',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="p-8 cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
|
||||
@click="setFocusedStep(5)"
|
||||
>
|
||||
<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="
|
||||
canComplete
|
||||
? '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="canComplete" name="i-heroicons-check" class="w-4 h-4" />
|
||||
<span v-else>5</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
class="text-2xl font-black text-black dark:text-white uppercase tracking-wide"
|
||||
>
|
||||
Review & finish
|
||||
</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 === 5 }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="focusedStep === 5"
|
||||
class="p-8 bg-neutral-50 dark:bg-neutral-900 border-t-2 border-black dark:border-white"
|
||||
>
|
||||
<WizardReviewStep @complete="completeWizard" @reset="resetWizard" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Actions -->
|
||||
<div class="flex justify-between items-center pt-8">
|
||||
<button class="export-btn" @click="resetWizard" :disabled="isResetting">
|
||||
Start Over
|
||||
</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>
|
||||
|
||||
<button v-if="canComplete" class="export-btn primary" @click="completeWizard">
|
||||
Complete Setup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Stores
|
||||
const membersStore = useMembersStore();
|
||||
const policiesStore = usePoliciesStore();
|
||||
const streamsStore = useStreamsStore();
|
||||
const budgetStore = useBudgetStore();
|
||||
const coopBuilderStore = useCoopBuilderStore();
|
||||
|
||||
// UI state
|
||||
const focusedStep = ref(1);
|
||||
const saveStatus = ref("");
|
||||
const isResetting = ref(false);
|
||||
const isCompleted = ref(false);
|
||||
|
||||
// Computed validation
|
||||
const canComplete = computed(
|
||||
() => membersStore.isValid && policiesStore.isValid && streamsStore.hasValidStreams
|
||||
);
|
||||
|
||||
// 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) {
|
||||
// Toggle if clicking on already focused step
|
||||
if (focusedStep.value === step) {
|
||||
focusedStep.value = 0; // Close the section
|
||||
} else {
|
||||
focusedStep.value = step; // Open the section
|
||||
}
|
||||
}
|
||||
|
||||
function completeWizard() {
|
||||
// Mark setup as complete and show restart button for testing
|
||||
isCompleted.value = true;
|
||||
}
|
||||
|
||||
async function resetWizard() {
|
||||
isResetting.value = true;
|
||||
|
||||
// Reset all stores
|
||||
membersStore.resetMembers();
|
||||
policiesStore.resetPolicies();
|
||||
streamsStore.resetStreams();
|
||||
budgetStore.resetBudgetOverhead();
|
||||
|
||||
// Reset coop builder state
|
||||
coopBuilderStore.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 all stores and coop builder state
|
||||
membersStore.resetMembers();
|
||||
policiesStore.resetPolicies();
|
||||
streamsStore.resetStreams();
|
||||
budgetStore.resetBudgetOverhead();
|
||||
coopBuilderStore.reset();
|
||||
saveStatus.value = "";
|
||||
|
||||
// Small delay for UX
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
isResetting.value = false;
|
||||
}
|
||||
|
||||
// SEO
|
||||
useSeoMeta({
|
||||
title: "Co-op Builder - Build Your Financial Foundation",
|
||||
description:
|
||||
"Build your co-op's financial foundation: set up members, policies, costs, and revenue streams.",
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<WizardPage />
|
||||
<CoopBuilderPage />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Reuse the existing wizard content by importing it as a component
|
||||
import WizardPage from "~/pages/wizard.vue";
|
||||
// Reuse the existing coop builder content by importing it as a component
|
||||
import CoopBuilderPage from "~/pages/coop-builder.vue";
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -344,7 +344,7 @@ const streamsStore = useStreamsStore();
|
|||
const budgetStore = useBudgetStore();
|
||||
const cashStore = useCashStore();
|
||||
const sessionStore = useSessionStore();
|
||||
const wizardStore = useWizardStore();
|
||||
const coopBuilderStore = useCoopBuilderStore();
|
||||
|
||||
const isResetting = ref(false);
|
||||
|
||||
|
|
@ -556,7 +556,7 @@ async function restartWizard() {
|
|||
sessionStore.resetSession();
|
||||
|
||||
// Reset wizard state
|
||||
wizardStore.reset();
|
||||
coopBuilderStore.reset();
|
||||
|
||||
// Small delay for UX
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -222,29 +222,7 @@ useHead({
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Ubuntu font import */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&family=Ubuntu+Mono:wght@400;700&display=swap");
|
||||
|
||||
/* Removed full-screen dither pattern to avoid gray haze in dark mode */
|
||||
|
||||
/* Exact shadow style from value-flow inspiration */
|
||||
.dither-shadow {
|
||||
background: black;
|
||||
background-image: radial-gradient(white 1px, transparent 1px);
|
||||
background-size: 2px 2px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dither-shadow {
|
||||
background: white;
|
||||
background-image: radial-gradient(black 1px, transparent 1px);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dark) .dither-shadow {
|
||||
background: white;
|
||||
background-image: radial-gradient(black 1px, transparent 1px);
|
||||
}
|
||||
/* Template index specific styles - no longer duplicated in main.css */
|
||||
|
||||
.dither-shadow-disabled {
|
||||
background: black;
|
||||
|
|
@ -265,74 +243,6 @@ useHead({
|
|||
background-image: radial-gradient(black 1px, transparent 1px);
|
||||
}
|
||||
|
||||
/* Rely on Tailwind bg utilities on container */
|
||||
|
||||
.template-card {
|
||||
@apply relative;
|
||||
font-family: "Ubuntu", monospace;
|
||||
}
|
||||
|
||||
.help-section {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.coming-soon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.dither-tag {
|
||||
position: relative;
|
||||
background: white;
|
||||
}
|
||||
.dither-tag::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent 0px,
|
||||
transparent 1px,
|
||||
black 1px,
|
||||
black 2px
|
||||
);
|
||||
opacity: 0.1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Button styling - pure bitmap, no colors */
|
||||
.bitmap-button {
|
||||
font-family: "Ubuntu Mono", monospace !important;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bitmap-button:hover {
|
||||
transform: translateY(-1px) translateX(-1px);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.bitmap-button:hover::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
border: 1px solid black;
|
||||
background: white;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.disabled-button {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Remove any inherited rounded corners */
|
||||
.template-card > *,
|
||||
.help-section > *,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- Wizard Subnav -->
|
||||
<WizardSubnav />
|
||||
|
||||
<!-- Export Options - Top -->
|
||||
<ExportOptions
|
||||
:export-data="exportData"
|
||||
filename="membership-agreement"
|
||||
title="Membership Agreement"
|
||||
/>
|
||||
title="Membership Agreement" />
|
||||
|
||||
<div
|
||||
class="template-wrapper bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
class="template-wrapper bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100">
|
||||
<!-- Document Container -->
|
||||
<div class="document-page">
|
||||
<div class="template-content">
|
||||
|
|
@ -20,8 +15,9 @@
|
|||
<div class="text-center mb-8">
|
||||
<h1
|
||||
class="text-3xl md:text-5xl font-bold uppercase text-neutral-900 dark:text-white m-0 py-4 border-t-2 border-b-2 border-neutral-900 dark:border-neutral-100"
|
||||
:data-coop-name="formData.cooperativeName || 'Worker Cooperative'"
|
||||
>
|
||||
:data-coop-name="
|
||||
formData.cooperativeName || 'Worker Cooperative'
|
||||
">
|
||||
MEMBERSHIP AGREEMENT
|
||||
</h1>
|
||||
</div>
|
||||
|
|
@ -29,8 +25,7 @@
|
|||
<!-- Section 1: Who We Are -->
|
||||
<div class="section-card">
|
||||
<h2
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
|
||||
>
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
|
||||
1. Who We Are
|
||||
</h2>
|
||||
|
||||
|
|
@ -42,8 +37,7 @@
|
|||
size="xl"
|
||||
class="w-full"
|
||||
@input="debouncedAutoSave"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Date Established" class="form-group-large">
|
||||
|
|
@ -52,8 +46,7 @@
|
|||
type="date"
|
||||
size="xl"
|
||||
class="large-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Our Purpose" class="form-group-large">
|
||||
|
|
@ -64,8 +57,7 @@
|
|||
size="xl"
|
||||
class="large-field"
|
||||
@input="debouncedAutoSave"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Our Core Values" class="form-group-large">
|
||||
|
|
@ -76,8 +68,7 @@
|
|||
size="xl"
|
||||
class="large-field"
|
||||
@input="debouncedAutoSave"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
|
||||
<div class="form-group-large">
|
||||
|
|
@ -91,8 +82,7 @@
|
|||
size="sm"
|
||||
color="primary"
|
||||
variant="outline"
|
||||
icon="i-heroicons-plus"
|
||||
>
|
||||
icon="i-heroicons-plus">
|
||||
Add Member
|
||||
</UButton>
|
||||
</div>
|
||||
|
|
@ -101,10 +91,10 @@
|
|||
<div
|
||||
v-for="(member, index) in formData.members"
|
||||
:key="index"
|
||||
class="border border-neutral-600 rounded-lg p-4"
|
||||
>
|
||||
class="border border-neutral-600 rounded-lg p-4">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<h4 class="font-medium text-neutral-900 dark:text-neutral-100">
|
||||
<h4
|
||||
class="font-medium text-neutral-900 dark:text-neutral-100">
|
||||
Member {{ index + 1 }}
|
||||
</h4>
|
||||
<UButton
|
||||
|
|
@ -113,8 +103,7 @@
|
|||
size="sm"
|
||||
color="red"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-trash"
|
||||
>
|
||||
icon="i-heroicons-trash">
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
|
|
@ -126,8 +115,7 @@
|
|||
size="xl"
|
||||
class="large-field"
|
||||
@input="debouncedAutoSave"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Email" class="form-group-large">
|
||||
|
|
@ -138,8 +126,7 @@
|
|||
size="xl"
|
||||
class="large-field"
|
||||
@input="debouncedAutoSave"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Join Date" class="form-group-large">
|
||||
|
|
@ -148,22 +135,19 @@
|
|||
type="date"
|
||||
size="xl"
|
||||
class="large-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Current Role (Optional)"
|
||||
class="form-group-large"
|
||||
>
|
||||
class="form-group-large">
|
||||
<UInput
|
||||
v-model="member.role"
|
||||
placeholder="e.g., Coordinator, Developer, etc."
|
||||
size="xl"
|
||||
class="large-field"
|
||||
@input="debouncedAutoSave"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -175,16 +159,14 @@
|
|||
<!-- Section 2: Membership -->
|
||||
<div class="section-card">
|
||||
<h2
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
|
||||
>
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
|
||||
2. Membership
|
||||
</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
Who Can Be a Member
|
||||
</h3>
|
||||
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
||||
|
|
@ -193,8 +175,8 @@
|
|||
<ul class="content-list my-2 pl-6 list-disc">
|
||||
<li>Shares our values and purpose</li>
|
||||
<li>
|
||||
Contributes labour to the cooperative (by doing actual work, not just
|
||||
investing money)
|
||||
Contributes labour to the cooperative (by doing actual work,
|
||||
not just investing money)
|
||||
</li>
|
||||
<li>Commits to collective decision-making</li>
|
||||
<li>Participates in governance responsibilities</li>
|
||||
|
|
@ -203,14 +185,14 @@
|
|||
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
Becoming a Member
|
||||
</h3>
|
||||
|
||||
<p class="content-paragraph">
|
||||
New members join through a consent process, which means existing members
|
||||
must agree that adding this person won't harm the cooperative.
|
||||
New members join through a consent process, which means
|
||||
existing members must agree that adding this person won't harm
|
||||
the cooperative.
|
||||
</p>
|
||||
|
||||
<ol class="content-list numbered my-2 pl-6 list-decimal">
|
||||
|
|
@ -221,8 +203,7 @@
|
|||
type="number"
|
||||
placeholder="3"
|
||||
class="inline-field number-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
months working together
|
||||
</li>
|
||||
<li>Values alignment conversation</li>
|
||||
|
|
@ -233,8 +214,7 @@
|
|||
type="number"
|
||||
placeholder="1000"
|
||||
class="inline-field number-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
(can be paid over time or waived based on need)
|
||||
</li>
|
||||
</ol>
|
||||
|
|
@ -242,20 +222,19 @@
|
|||
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
Leaving the Cooperative
|
||||
</h3>
|
||||
|
||||
<p class="content-paragraph flex items-baseline gap-2 flex-wrap">
|
||||
<p
|
||||
class="content-paragraph flex items-baseline gap-2 flex-wrap">
|
||||
Members can leave anytime with
|
||||
<UInput
|
||||
v-model="formData.noticeDays"
|
||||
type="number"
|
||||
placeholder="30"
|
||||
class="inline-field number-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
days notice. The cooperative will:
|
||||
</p>
|
||||
|
||||
|
|
@ -267,8 +246,7 @@
|
|||
type="number"
|
||||
placeholder="30"
|
||||
class="inline-field number-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
days
|
||||
</li>
|
||||
<li class="flex items-baseline gap-2 flex-wrap">
|
||||
|
|
@ -278,11 +256,12 @@
|
|||
type="number"
|
||||
placeholder="90"
|
||||
class="inline-field number-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
days
|
||||
</li>
|
||||
<li>Maintain respectful ongoing relationships when possible</li>
|
||||
<li>
|
||||
Maintain respectful ongoing relationships when possible
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -291,80 +270,71 @@
|
|||
<!-- Section 3: How We Make Decisions -->
|
||||
<div class="section-card">
|
||||
<h2
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
|
||||
>
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
|
||||
3. How We Make Decisions
|
||||
</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
Consent-Based Decisions
|
||||
</h3>
|
||||
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
||||
We use consent, not consensus. This means we move forward when no one
|
||||
has a principled objection that would harm the cooperative. An objection
|
||||
must explain how the proposal would contradict our values or threaten
|
||||
our sustainability.
|
||||
We use consent, not consensus. This means we move forward when
|
||||
no one has a principled objection that would harm the
|
||||
cooperative. An objection must explain how the proposal would
|
||||
contradict our values or threaten our sustainability.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
Day-to-Day Decisions
|
||||
</h3>
|
||||
<p
|
||||
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap"
|
||||
>
|
||||
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap">
|
||||
Decisions under $<UInput
|
||||
v-model="formData.dayToDayLimit"
|
||||
type="number"
|
||||
placeholder="100"
|
||||
class="inline-field number-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
can be made by any member. Just tell others what you did at the next
|
||||
meeting.
|
||||
@change="autoSave" />
|
||||
can be made by any member. Just tell others what you did at
|
||||
the next meeting.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
Regular Decisions
|
||||
</h3>
|
||||
<p
|
||||
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap"
|
||||
>
|
||||
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap">
|
||||
Decisions between $<UInput
|
||||
v-model="formData.regularDecisionMin"
|
||||
type="number"
|
||||
placeholder="100"
|
||||
class="inline-field number-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
and $<UInput
|
||||
v-model="formData.regularDecisionMax"
|
||||
type="number"
|
||||
placeholder="1000"
|
||||
class="inline-field number-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
need consent from members present at a meeting (minimum 2 members).
|
||||
@change="autoSave" />
|
||||
need consent from members present at a meeting (minimum 2
|
||||
members).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
Major Decisions
|
||||
</h3>
|
||||
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
||||
|
|
@ -379,8 +349,7 @@
|
|||
type="number"
|
||||
placeholder="5000"
|
||||
class="inline-field number-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
</li>
|
||||
<li>Fundamental changes to our purpose or structure</li>
|
||||
<li>Dissolution of the cooperative</li>
|
||||
|
|
@ -389,8 +358,7 @@
|
|||
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
Meeting Structure
|
||||
</h3>
|
||||
<ul class="content-list my-2 pl-6 list-disc">
|
||||
|
|
@ -400,8 +368,7 @@
|
|||
v-model="formData.meetingFrequency"
|
||||
placeholder="weekly"
|
||||
class="inline-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
</li>
|
||||
<li class="flex items-baseline gap-2 flex-wrap">
|
||||
Emergency meetings need
|
||||
|
|
@ -410,12 +377,13 @@
|
|||
type="number"
|
||||
placeholder="24"
|
||||
class="inline-field number-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
hours notice
|
||||
</li>
|
||||
<li>We rotate who facilitates meetings</li>
|
||||
<li>Decisions and reasoning get documented in shared notes</li>
|
||||
<li>
|
||||
Decisions and reasoning get documented in shared notes
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -424,28 +392,25 @@
|
|||
<!-- Section 4: Money and Labour -->
|
||||
<div class="section-card">
|
||||
<h2
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
|
||||
>
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
|
||||
4. Money and Labour
|
||||
</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
Equal Ownership
|
||||
</h3>
|
||||
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
||||
Each member owns an equal share of the cooperative, regardless of hours
|
||||
worked or tenure.
|
||||
Each member owns an equal share of the cooperative, regardless
|
||||
of hours worked or tenure.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
Paying Ourselves
|
||||
</h3>
|
||||
<ul class="content-list my-2 pl-6 list-disc">
|
||||
|
|
@ -455,8 +420,7 @@
|
|||
type="number"
|
||||
placeholder="25"
|
||||
class="inline-field number-field"
|
||||
@change="autoSave"
|
||||
/>/hour for all members
|
||||
@change="autoSave" />/hour for all members
|
||||
</li>
|
||||
<li class="flex items-baseline gap-2 flex-wrap">
|
||||
Or: Equal monthly draw of $<UInput
|
||||
|
|
@ -464,8 +428,7 @@
|
|||
type="number"
|
||||
placeholder="2000"
|
||||
class="inline-field number-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
per member
|
||||
</li>
|
||||
<li class="flex items-baseline gap-2 flex-wrap">
|
||||
|
|
@ -475,8 +438,7 @@
|
|||
:items="dayOptions"
|
||||
placeholder="15"
|
||||
class="inline-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
of each month
|
||||
</li>
|
||||
<li class="flex items-baseline gap-2 flex-wrap">
|
||||
|
|
@ -485,8 +447,7 @@
|
|||
v-model="formData.surplusFrequency"
|
||||
placeholder="quarter"
|
||||
class="inline-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -494,8 +455,7 @@
|
|||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
Work Expectations
|
||||
</h3>
|
||||
<ul class="content-list my-2 pl-6 list-disc">
|
||||
|
|
@ -506,29 +466,31 @@
|
|||
type="number"
|
||||
placeholder="40"
|
||||
class="inline-field number-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
(flexible based on capacity)
|
||||
</li>
|
||||
<li>We explicitly reject crunch culture</li>
|
||||
<li>Members communicate their capacity openly</li>
|
||||
<li>
|
||||
We adjust workload collectively when someone needs reduced hours
|
||||
We adjust workload collectively when someone needs reduced
|
||||
hours
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
Financial Transparency
|
||||
</h3>
|
||||
<ul class="content-list my-2 pl-6 list-disc">
|
||||
<li>All members can access all financial records anytime</li>
|
||||
<li>
|
||||
All members can access all financial records anytime
|
||||
</li>
|
||||
<li>Monthly financial check-ins at meetings</li>
|
||||
<li>
|
||||
Quarterly reviews of our runway (how many months we can operate)
|
||||
Quarterly reviews of our runway (how many months we can
|
||||
operate)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -539,34 +501,31 @@
|
|||
<!-- Section 5: Roles and Responsibilities -->
|
||||
<div class="section-card">
|
||||
<h2
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
|
||||
>
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
|
||||
5. Roles and Responsibilities
|
||||
</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
Rotating Roles
|
||||
</h3>
|
||||
<p
|
||||
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap"
|
||||
>
|
||||
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap">
|
||||
We rotate operational roles every
|
||||
<UInput
|
||||
v-model="formData.roleRotationMonths"
|
||||
type="number"
|
||||
placeholder="6"
|
||||
class="inline-field number-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
months. Current roles include:
|
||||
</p>
|
||||
<ul class="content-list">
|
||||
<li>
|
||||
Financial coordinator (handles bookkeeping, not financial decisions)
|
||||
Financial coordinator (handles bookkeeping, not financial
|
||||
decisions)
|
||||
</li>
|
||||
<li>Meeting facilitator</li>
|
||||
<li>External communications</li>
|
||||
|
|
@ -576,8 +535,7 @@
|
|||
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
Shared Responsibilities
|
||||
</h3>
|
||||
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
||||
|
|
@ -595,16 +553,14 @@
|
|||
<!-- Section 6: Conflict and Care -->
|
||||
<div class="section-card">
|
||||
<h2
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
|
||||
>
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
|
||||
6. Conflict and Care
|
||||
</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
When Conflict Happens
|
||||
</h3>
|
||||
<ol class="content-list numbered my-2 pl-6 list-decimal">
|
||||
|
|
@ -617,15 +573,16 @@
|
|||
|
||||
<div>
|
||||
<h3
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3"
|
||||
>
|
||||
class="subsection-title text-xl font-semibold text-neutral-700 dark:text-neutral-200 border-b border-neutral-200 dark:border-neutral-600 pb-1 mb-3">
|
||||
Care Commitments
|
||||
</h3>
|
||||
<ul class="content-list my-2 pl-6 list-disc">
|
||||
<li>We check in about capacity and wellbeing regularly</li>
|
||||
<li>We honour diverse access needs</li>
|
||||
<li>We maintain flexibility for life circumstances</li>
|
||||
<li>We contribute to mutual aid when members face hardship</li>
|
||||
<li>
|
||||
We contribute to mutual aid when members face hardship
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -634,31 +591,27 @@
|
|||
<!-- Section 7: Changing This Agreement -->
|
||||
<div class="section-card">
|
||||
<h2
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
|
||||
>
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
|
||||
7. Changing This Agreement
|
||||
</h2>
|
||||
|
||||
<p
|
||||
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap"
|
||||
>
|
||||
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap">
|
||||
This is a living document. We review it every
|
||||
<UInput
|
||||
v-model="formData.reviewFrequency"
|
||||
placeholder="year"
|
||||
class="inline-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
and update it through our consent process. Small clarifications can happen
|
||||
anytime; structural changes need full member consent.
|
||||
@change="autoSave" />
|
||||
and update it through our consent process. Small clarifications
|
||||
can happen anytime; structural changes need full member consent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Section 8: If We Need to Close -->
|
||||
<div class="section-card">
|
||||
<h2
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
|
||||
>
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
|
||||
8. If We Need to Close
|
||||
</h2>
|
||||
|
||||
|
|
@ -675,8 +628,7 @@
|
|||
<UInput
|
||||
v-model="formData.assetDonationTarget"
|
||||
placeholder="Enter organization name"
|
||||
class="inline-field wide-field"
|
||||
/>
|
||||
class="inline-field wide-field" />
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
|
@ -685,8 +637,7 @@
|
|||
<!-- Section 9: Legal Bits -->
|
||||
<div class="section-card">
|
||||
<h2
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
|
||||
>
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
|
||||
9. Legal Bits
|
||||
</h2>
|
||||
|
||||
|
|
@ -697,8 +648,7 @@
|
|||
v-model="formData.legalStructure"
|
||||
size="xl"
|
||||
class="w-full"
|
||||
placeholder="Cooperative corporation, LLC, partnership, etc."
|
||||
/>
|
||||
placeholder="Cooperative corporation, LLC, partnership, etc." />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Registered in" class="form-group-inline">
|
||||
|
|
@ -707,8 +657,7 @@
|
|||
placeholder="State/Province"
|
||||
size="xl"
|
||||
class="inline-field w-full"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
|
||||
<div class="fiscal-year-group">
|
||||
|
|
@ -720,25 +669,23 @@
|
|||
placeholder="Month"
|
||||
size="xl"
|
||||
class="w-60"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
<USelect
|
||||
v-model="formData.fiscalYearEndDay"
|
||||
:items="dayOptions"
|
||||
placeholder="Day"
|
||||
size="xl"
|
||||
class="w-40"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
</div>
|
||||
</UFormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
||||
This agreement works alongside but doesn't replace our legal incorporation
|
||||
documents. Where they conflict, we follow the law but work to align our
|
||||
legal structure with our values.
|
||||
This agreement works alongside but doesn't replace our legal
|
||||
incorporation documents. Where they conflict, we follow the law
|
||||
but work to align our legal structure with our values.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -746,46 +693,39 @@
|
|||
<!-- Section 10: Agreement Review -->
|
||||
<div class="section-card">
|
||||
<h2
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4"
|
||||
>
|
||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
|
||||
10. Agreement Review
|
||||
</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
||||
By using this agreement, we commit to these principles and to showing up
|
||||
for each other.
|
||||
By using this agreement, we commit to these principles and to
|
||||
showing up for each other.
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="bg-neutral-50 dark:bg-neutral-900 p-4 rounded-md border-l-4 border-emerald-300"
|
||||
>
|
||||
class="bg-neutral-50 dark:bg-neutral-900 p-4 rounded-md border-l-4 border-emerald-300">
|
||||
<p
|
||||
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap"
|
||||
>
|
||||
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap">
|
||||
This agreement was last updated on
|
||||
<UInput
|
||||
v-model="formData.lastUpdated"
|
||||
type="date"
|
||||
class="inline-field"
|
||||
@change="autoSave"
|
||||
/>. We commit to reviewing it on
|
||||
@change="autoSave" />. We commit to reviewing it on
|
||||
<UInput
|
||||
v-model="formData.nextReview"
|
||||
type="date"
|
||||
class="inline-field"
|
||||
@change="autoSave"
|
||||
/>
|
||||
@change="autoSave" />
|
||||
or sooner if circumstances require.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="signature-space mt-8 p-8 border border-dashed border-neutral-300 rounded-md bg-neutral-50 dark:bg-neutral-950"
|
||||
>
|
||||
class="signature-space mt-8 p-8 border border-dashed border-neutral-300 rounded-md bg-neutral-50 dark:bg-neutral-950">
|
||||
<p
|
||||
class="content-paragraph mb-3 leading-relaxed text-center text-neutral-600 italic"
|
||||
>
|
||||
class="content-paragraph mb-3 leading-relaxed text-center text-neutral-600 italic">
|
||||
[Space for member signatures when printed]
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -799,8 +739,7 @@
|
|||
<ExportOptions
|
||||
:export-data="exportData"
|
||||
filename="membership-agreement"
|
||||
title="Membership Agreement"
|
||||
/>
|
||||
title="Membership Agreement" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -898,7 +837,10 @@ onMounted(() => {
|
|||
|
||||
// Auto-save individual field changes immediately
|
||||
const autoSave = () => {
|
||||
localStorage.setItem("membership-agreement-data", JSON.stringify(formData.value));
|
||||
localStorage.setItem(
|
||||
"membership-agreement-data",
|
||||
JSON.stringify(formData.value)
|
||||
);
|
||||
console.log("Manual auto-save triggered:", formData.value);
|
||||
};
|
||||
|
||||
|
|
@ -950,8 +892,12 @@ const handlePrint = () => {
|
|||
`;
|
||||
|
||||
// Add signature lines for each member
|
||||
const membersWithNames = formData.value.members?.filter((m) => m.name) || [];
|
||||
const numSignatures = Math.max(2, Math.min(8, membersWithNames.length || 4));
|
||||
const membersWithNames =
|
||||
formData.value.members?.filter((m) => m.name) || [];
|
||||
const numSignatures = Math.max(
|
||||
2,
|
||||
Math.min(8, membersWithNames.length || 4)
|
||||
);
|
||||
|
||||
for (let i = 0; i < numSignatures; i++) {
|
||||
const memberName = membersWithNames[i]?.name || "";
|
||||
|
|
@ -981,7 +927,8 @@ const handlePrint = () => {
|
|||
value = formData.value.cooperativeName;
|
||||
else if (input.closest('[label="Date Established"]'))
|
||||
value = formData.value.dateEstablished;
|
||||
else if (input.closest('[label="Our Purpose"]')) value = formData.value.purpose;
|
||||
else if (input.closest('[label="Our Purpose"]'))
|
||||
value = formData.value.purpose;
|
||||
else if (input.closest('[label="Our Core Values"]'))
|
||||
value = formData.value.coreValues;
|
||||
else if (input.closest('[label="Legal Structure"]'))
|
||||
|
|
@ -998,13 +945,16 @@ const handlePrint = () => {
|
|||
// Handle member fields
|
||||
else if (input.closest(".border-neutral-200")) {
|
||||
const memberCard = input.closest(".border-neutral-200");
|
||||
const memberIndex = Array.from(memberCard.parentNode.children).indexOf(memberCard);
|
||||
const memberIndex = Array.from(memberCard.parentNode.children).indexOf(
|
||||
memberCard
|
||||
);
|
||||
const member = formData.value.members?.[memberIndex];
|
||||
if (member) {
|
||||
if (input.closest('[label="Full Name"]')) value = member.name;
|
||||
else if (input.closest('[label="Email"]')) value = member.email;
|
||||
else if (input.closest('[label="Join Date"]')) value = member.joinDate;
|
||||
else if (input.closest('[label="Current Role (Optional)"]')) value = member.role;
|
||||
else if (input.closest('[label="Current Role (Optional)"]'))
|
||||
value = member.role;
|
||||
}
|
||||
}
|
||||
// Fallback to input.value
|
||||
|
|
@ -1143,11 +1093,13 @@ const exportData = computed(() => ({
|
|||
|
||||
.template-content.font-ubuntu,
|
||||
.template-content.font-ubuntu * {
|
||||
font-family: "Ubuntu", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
|
||||
font-family: "Ubuntu", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
sans-serif !important;
|
||||
}
|
||||
|
||||
.template-content.font-inter,
|
||||
.template-content.font-inter * {
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
sans-serif !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,26 +1,20 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- Wizard Subnav -->
|
||||
<WizardSubnav />
|
||||
|
||||
<!-- Export Options - Top -->
|
||||
<ExportOptions
|
||||
:export-data="exportData"
|
||||
filename="tech-charter"
|
||||
title="Technology Charter"
|
||||
/>
|
||||
title="Technology Charter" />
|
||||
|
||||
<div
|
||||
class="template-wrapper bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
class="template-wrapper bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100">
|
||||
<!-- Document Container -->
|
||||
<div class="document-page">
|
||||
<div class="template-content">
|
||||
<!-- Document Header -->
|
||||
<div class="text-center mb-8">
|
||||
<h1
|
||||
class="text-3xl md:text-5xl font-bold uppercase text-neutral-900 dark:text-white m-0 py-4 border-t-2 border-b-2 border-neutral-900 dark:border-neutral-100"
|
||||
>
|
||||
class="text-3xl md:text-5xl font-bold uppercase text-neutral-900 dark:text-white m-0 py-4 border-t-2 border-b-2 border-neutral-900 dark:border-neutral-100">
|
||||
Tech Charter
|
||||
</h1>
|
||||
</div>
|
||||
|
|
@ -30,9 +24,12 @@
|
|||
<!-- Purpose Section -->
|
||||
<div class="section-card">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-neutral-800 mb-4">Charter Purpose</h2>
|
||||
<h2 class="text-2xl font-bold text-neutral-800 mb-4">
|
||||
Charter Purpose
|
||||
</h2>
|
||||
<p class="text-neutral-600 mb-4">
|
||||
Describe what this charter will guide and why it matters to your group.
|
||||
Describe what this charter will guide and why it matters to
|
||||
your group.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -40,8 +37,7 @@
|
|||
<textarea
|
||||
v-model="charterPurpose"
|
||||
class="w-full min-h-32 p-4 border-2 border-neutral-300 bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100 focus:border-black dark:focus:border-white transition-colors resize-y"
|
||||
rows="4"
|
||||
/>
|
||||
rows="4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -52,37 +48,39 @@
|
|||
Define Your Principles & Importance
|
||||
</h2>
|
||||
<p class="text-neutral-600 mb-6">
|
||||
Select principles and set their importance. Zero means excluded, 5 means
|
||||
critical.
|
||||
Select principles and set their importance. Zero means
|
||||
excluded, 5 means critical.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-1 gap-4">
|
||||
<div v-for="principle in principles" :key="principle.id" class="relative">
|
||||
<div
|
||||
v-for="principle in principles"
|
||||
:key="principle.id"
|
||||
class="relative">
|
||||
<!-- Dithered shadow for selected cards -->
|
||||
<div
|
||||
v-if="principleWeights[principle.id] > 0"
|
||||
class="absolute top-2 left-2 w-full h-full dither-shadow"
|
||||
></div>
|
||||
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
||||
|
||||
<div
|
||||
:class="[
|
||||
'relative transition-all',
|
||||
principleWeights[principle.id] > 0
|
||||
? 'principle-selected border-2 border-black dark:border-white bg-white dark:bg-neutral-950'
|
||||
? 'item-selected border-2 border-black dark:border-white bg-white dark:bg-neutral-950'
|
||||
: 'border border-black dark:border-white bg-transparent',
|
||||
]"
|
||||
>
|
||||
]">
|
||||
<div class="p-6">
|
||||
<div class="flex items-start gap-6">
|
||||
<!-- Principle info -->
|
||||
<div class="flex-1">
|
||||
<div
|
||||
:class="[
|
||||
'principle-text-bg mb-3',
|
||||
principleWeights[principle.id] > 0 ? 'selected' : '',
|
||||
]"
|
||||
>
|
||||
'item-text-bg mb-3',
|
||||
principleWeights[principle.id] > 0
|
||||
? 'selected'
|
||||
: '',
|
||||
]">
|
||||
<h3 class="font-bold text-lg mb-2">
|
||||
{{ principle.name }}
|
||||
</h3>
|
||||
|
|
@ -92,8 +90,7 @@
|
|||
? 'text-neutral-700'
|
||||
: 'text-neutral-600'
|
||||
"
|
||||
class="text-sm"
|
||||
>
|
||||
class="text-sm">
|
||||
{{ principle.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -102,8 +99,7 @@
|
|||
<!-- Importance selector -->
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<label
|
||||
class="text-xs font-bold text-neutral-500 uppercase tracking-wider"
|
||||
>
|
||||
class="text-xs font-bold text-neutral-500 uppercase tracking-wider">
|
||||
Importance
|
||||
</label>
|
||||
|
||||
|
|
@ -119,8 +115,7 @@
|
|||
? 'bg-black text-white border-black dark:bg-white dark:text-black dark:border-white'
|
||||
: 'bg-white border-neutral-300 hover:border-neutral-500 dark:bg-neutral-950',
|
||||
]"
|
||||
:title="`Set importance to ${level}`"
|
||||
>
|
||||
:title="`Set importance to ${level}`">
|
||||
{{ level }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -131,7 +126,11 @@
|
|||
{{ principleWeights[principle.id] || 0 }}
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500">
|
||||
{{ getWeightLabel(principleWeights[principle.id] || 0) }}
|
||||
{{
|
||||
getWeightLabel(
|
||||
principleWeights[principle.id] || 0
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -140,20 +139,19 @@
|
|||
<!-- Non-negotiable toggle (only shows for weights > 0) -->
|
||||
<div
|
||||
v-if="principleWeights[principle.id] > 0"
|
||||
class="mt-4 pt-4 border-t border-neutral-200"
|
||||
>
|
||||
class="mt-4 pt-4 border-t border-neutral-200">
|
||||
<label
|
||||
:class="[
|
||||
'flex items-center gap-3 cursor-pointer principle-label-bg px-2 py-1',
|
||||
nonNegotiables.includes(principle.id) ? 'selected' : '',
|
||||
]"
|
||||
>
|
||||
'flex items-center gap-3 cursor-pointer item-label-bg px-2 py-1',
|
||||
nonNegotiables.includes(principle.id)
|
||||
? 'selected'
|
||||
: '',
|
||||
]">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="nonNegotiables.includes(principle.id)"
|
||||
@change="toggleNonNegotiable(principle.id)"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
class="w-4 h-4" />
|
||||
<span class="text-sm font-medium text-red-600">
|
||||
Make this non-negotiable
|
||||
</span>
|
||||
|
|
@ -163,9 +161,9 @@
|
|||
<!-- Show rubric description when selected -->
|
||||
<div
|
||||
v-if="principleWeights[principle.id] > 0"
|
||||
class="mt-4 p-3 principle-label-bg selected border border-neutral-200"
|
||||
>
|
||||
<div class="text-xs font-bold uppercase text-neutral-500 mb-1">
|
||||
class="mt-4 p-3 item-label-bg selected border border-neutral-200">
|
||||
<div
|
||||
class="text-xs font-bold uppercase text-neutral-500 mb-1">
|
||||
Evaluation Criteria:
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
|
|
@ -183,8 +181,7 @@
|
|||
<div>
|
||||
<h2
|
||||
class="text-2xl font-bold text-neutral-800 mb-2"
|
||||
id="constraints-heading"
|
||||
>
|
||||
id="constraints-heading">
|
||||
Technical Constraints
|
||||
</h2>
|
||||
</div>
|
||||
|
|
@ -195,18 +192,15 @@
|
|||
<div
|
||||
class="flex flex-wrap gap-3 constraint-buttons"
|
||||
role="radiogroup"
|
||||
aria-labelledby="auth-heading"
|
||||
>
|
||||
aria-labelledby="auth-heading">
|
||||
<div
|
||||
v-for="option in authOptions"
|
||||
:key="option.value"
|
||||
class="relative"
|
||||
>
|
||||
class="relative">
|
||||
<!-- Dithered shadow for selected buttons -->
|
||||
<div
|
||||
v-if="constraints.sso === option.value"
|
||||
class="absolute top-2 left-2 w-full h-full dither-shadow"
|
||||
></div>
|
||||
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
||||
<button
|
||||
@click="constraints.sso = option.value"
|
||||
:aria-pressed="constraints.sso === option.value"
|
||||
|
|
@ -217,8 +211,7 @@
|
|||
constraints.sso === option.value
|
||||
? 'constraint-selected border-black dark:border-white cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
|
||||
: 'border-neutral-300 hover:border-neutral-400 bg-white dark:bg-neutral-950 cursor-pointer',
|
||||
]"
|
||||
>
|
||||
]">
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -230,18 +223,15 @@
|
|||
<div
|
||||
class="flex flex-wrap gap-3 constraint-buttons"
|
||||
role="radiogroup"
|
||||
aria-labelledby="hosting-heading"
|
||||
>
|
||||
aria-labelledby="hosting-heading">
|
||||
<div
|
||||
v-for="option in hostingOptions"
|
||||
:key="option.value"
|
||||
class="relative"
|
||||
>
|
||||
class="relative">
|
||||
<!-- Dithered shadow for selected buttons -->
|
||||
<div
|
||||
v-if="constraints.hosting === option.value"
|
||||
class="absolute top-2 left-2 w-full h-full dither-shadow"
|
||||
></div>
|
||||
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
||||
<button
|
||||
@click="constraints.hosting = option.value"
|
||||
:aria-pressed="constraints.hosting === option.value"
|
||||
|
|
@ -252,8 +242,7 @@
|
|||
constraints.hosting === option.value
|
||||
? 'constraint-selected border-black dark:border-white cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
|
||||
: 'border-neutral-300 hover:border-neutral-400 bg-white dark:bg-neutral-950 cursor-pointer',
|
||||
]"
|
||||
>
|
||||
]">
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -261,29 +250,32 @@
|
|||
</fieldset>
|
||||
|
||||
<fieldset class="bg-neutral-50 p-6 rounded-lg">
|
||||
<legend class="font-semibold text-lg">Required Integrations</legend>
|
||||
<p class="text-sm text-neutral-600 mb-4">Select all that apply</p>
|
||||
<legend class="font-semibold text-lg">
|
||||
Required Integrations
|
||||
</legend>
|
||||
<p class="text-sm text-neutral-600 mb-4">
|
||||
Select all that apply
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-3 constraint-buttons">
|
||||
<div
|
||||
v-for="integration in integrationOptions"
|
||||
:key="integration"
|
||||
class="relative"
|
||||
>
|
||||
class="relative">
|
||||
<!-- Dithered shadow for selected buttons -->
|
||||
<div
|
||||
v-if="constraints.integrations.includes(integration)"
|
||||
class="absolute top-2 left-2 w-full h-full dither-shadow"
|
||||
></div>
|
||||
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
||||
<button
|
||||
@click="toggleIntegration(integration)"
|
||||
:aria-pressed="constraints.integrations.includes(integration)"
|
||||
:aria-pressed="
|
||||
constraints.integrations.includes(integration)
|
||||
"
|
||||
:class="[
|
||||
'relative px-5 py-2 border-2 transition-all focus:outline-none focus:ring-1 focus:ring-black cursor-pointer',
|
||||
constraints.integrations.includes(integration)
|
||||
? 'constraint-selected border-black dark:border-white cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
|
||||
: 'border-neutral-300 hover:border-neutral-400 bg-white dark:bg-neutral-950 cursor-pointer',
|
||||
]"
|
||||
>
|
||||
]">
|
||||
{{ integration }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -291,22 +283,21 @@
|
|||
</fieldset>
|
||||
|
||||
<fieldset class="bg-neutral-50 p-6 rounded-lg">
|
||||
<legend class="font-semibold text-lg">Support Expectations</legend>
|
||||
<legend class="font-semibold text-lg">
|
||||
Support Expectations
|
||||
</legend>
|
||||
<div
|
||||
class="flex flex-wrap gap-3 constraint-buttons"
|
||||
role="radiogroup"
|
||||
aria-labelledby="support-heading"
|
||||
>
|
||||
aria-labelledby="support-heading">
|
||||
<div
|
||||
v-for="option in supportOptions"
|
||||
:key="option.value"
|
||||
class="relative"
|
||||
>
|
||||
class="relative">
|
||||
<!-- Dithered shadow for selected buttons -->
|
||||
<div
|
||||
v-if="constraints.support === option.value"
|
||||
class="absolute top-2 left-2 w-full h-full dither-shadow"
|
||||
></div>
|
||||
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
||||
<button
|
||||
@click="constraints.support = option.value"
|
||||
:aria-pressed="constraints.support === option.value"
|
||||
|
|
@ -317,8 +308,7 @@
|
|||
constraints.support === option.value
|
||||
? 'constraint-selected border-black dark:border-white cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
|
||||
: 'border-neutral-300 hover:border-neutral-400 bg-white dark:bg-neutral-950 cursor-pointer',
|
||||
]"
|
||||
>
|
||||
]">
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -326,22 +316,21 @@
|
|||
</fieldset>
|
||||
|
||||
<fieldset class="bg-neutral-50 p-6 rounded-lg">
|
||||
<legend class="font-semibold text-lg">Migration Timeline</legend>
|
||||
<legend class="font-semibold text-lg">
|
||||
Migration Timeline
|
||||
</legend>
|
||||
<div
|
||||
class="flex flex-wrap gap-3 constraint-buttons"
|
||||
role="radiogroup"
|
||||
aria-labelledby="timeline-heading"
|
||||
>
|
||||
aria-labelledby="timeline-heading">
|
||||
<div
|
||||
v-for="option in timelineOptions"
|
||||
:key="option.value"
|
||||
class="relative"
|
||||
>
|
||||
class="relative">
|
||||
<!-- Dithered shadow for selected buttons -->
|
||||
<div
|
||||
v-if="constraints.timeline === option.value"
|
||||
class="absolute top-2 left-2 w-full h-full dither-shadow"
|
||||
></div>
|
||||
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
||||
<button
|
||||
@click="constraints.timeline = option.value"
|
||||
:aria-pressed="constraints.timeline === option.value"
|
||||
|
|
@ -352,8 +341,7 @@
|
|||
constraints.timeline === option.value
|
||||
? 'constraint-selected border-black dark:border-white cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
|
||||
: 'border-neutral-300 hover:border-neutral-400 bg-white dark:bg-neutral-950 cursor-pointer',
|
||||
]"
|
||||
>
|
||||
]">
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -367,8 +355,7 @@
|
|||
<button
|
||||
@click="resetForm"
|
||||
class="export-btn"
|
||||
title="Clear all form data and start over"
|
||||
>
|
||||
title="Clear all form data and start over">
|
||||
<UIcon name="i-heroicons-arrow-path" />
|
||||
Reset Form
|
||||
</button>
|
||||
|
|
@ -381,17 +368,19 @@
|
|||
v-if="charterGenerated"
|
||||
class="relative animate-fadeIn"
|
||||
role="main"
|
||||
aria-label="Generated Technology Charter"
|
||||
>
|
||||
aria-label="Generated Technology Charter">
|
||||
<!-- Dithered shadow -->
|
||||
<div class="absolute top-4 left-4 right-0 bottom-0 dither-shadow"></div>
|
||||
<div
|
||||
class="absolute top-4 left-4 right-0 bottom-0 dither-shadow"></div>
|
||||
|
||||
<!-- Charter container -->
|
||||
<div
|
||||
class="relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-white p-8"
|
||||
>
|
||||
<div class="text-center mb-8 pb-6 border-b-2 border-black dark:border-white">
|
||||
<h2 class="text-3xl font-bold text-neutral-800" id="charter-title">
|
||||
class="relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-white p-8">
|
||||
<div
|
||||
class="text-center mb-8 pb-6 border-b-2 border-black dark:border-white">
|
||||
<h2
|
||||
class="text-3xl font-bold text-neutral-800"
|
||||
id="charter-title">
|
||||
Technology Charter
|
||||
</h2>
|
||||
<p class="text-neutral-600 mt-2">
|
||||
|
|
@ -407,8 +396,7 @@
|
|||
<div class="mt-4">
|
||||
<button
|
||||
@click="scrollToTop"
|
||||
class="text-sm text-neutral-600 hover:text-neutral-800 underline focus:outline-none focus:ring-2 focus:ring-neutral-500 rounded"
|
||||
>
|
||||
class="text-sm text-neutral-600 hover:text-neutral-800 underline focus:outline-none focus:ring-2 focus:ring-neutral-500 rounded">
|
||||
Back to form
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -418,10 +406,10 @@
|
|||
<section class="mb-8">
|
||||
<h3 class="text-xl font-bold text-neutral-800 mb-3">Purpose</h3>
|
||||
<p class="text-neutral-700 leading-relaxed">
|
||||
This charter guides our cooperative's technology decisions based on our
|
||||
shared values and operational needs. It ensures we choose tools that
|
||||
support our mission while respecting our principles of autonomy,
|
||||
sustainability, and mutual aid.
|
||||
This charter guides our cooperative's technology decisions
|
||||
based on our shared values and operational needs. It ensures
|
||||
we choose tools that support our mission while respecting our
|
||||
principles of autonomy, sustainability, and mutual aid.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -429,21 +417,25 @@
|
|||
class="mb-8"
|
||||
v-if="
|
||||
Object.keys(principleWeights).filter(
|
||||
(p) => principleWeights[p] > 0 && !nonNegotiables.includes(p)
|
||||
(p) =>
|
||||
principleWeights[p] > 0 && !nonNegotiables.includes(p)
|
||||
).length > 0
|
||||
"
|
||||
>
|
||||
<h3 class="text-xl font-bold text-neutral-800 mb-3">Core Principles</h3>
|
||||
">
|
||||
<h3 class="text-xl font-bold text-neutral-800 mb-3">
|
||||
Core Principles
|
||||
</h3>
|
||||
<ul class="space-y-2">
|
||||
<li
|
||||
v-for="principleId in Object.keys(principleWeights).filter(
|
||||
(p) => principleWeights[p] > 0 && !nonNegotiables.includes(p)
|
||||
(p) =>
|
||||
principleWeights[p] > 0 && !nonNegotiables.includes(p)
|
||||
)"
|
||||
:key="principleId"
|
||||
class="flex items-start"
|
||||
>
|
||||
class="flex items-start">
|
||||
<span class="text-neutral-600 mr-2">→</span>
|
||||
<span>{{ principles.find((p) => p.id === principleId)?.name }}</span>
|
||||
<span>{{
|
||||
principles.find((p) => p.id === principleId)?.name
|
||||
}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
|
@ -453,16 +445,18 @@
|
|||
Non-Negotiable Requirements
|
||||
</h3>
|
||||
<p class="text-red-600 font-semibold mb-3">
|
||||
Any vendor failing these requirements is automatically disqualified.
|
||||
Any vendor failing these requirements is automatically
|
||||
disqualified.
|
||||
</p>
|
||||
<ul class="space-y-2">
|
||||
<li
|
||||
v-for="principleId in nonNegotiables"
|
||||
:key="principleId"
|
||||
class="flex items-start text-red-600 font-semibold"
|
||||
>
|
||||
class="flex items-start text-red-600 font-semibold">
|
||||
<span class="mr-2">→</span>
|
||||
<span>{{ principles.find((p) => p.id === principleId)?.name }}</span>
|
||||
<span>{{
|
||||
principles.find((p) => p.id === principleId)?.name
|
||||
}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
|
@ -477,7 +471,8 @@
|
|||
<span
|
||||
>Authentication:
|
||||
{{
|
||||
authOptions.find((o) => o.value === constraints.sso)?.label
|
||||
authOptions.find((o) => o.value === constraints.sso)
|
||||
?.label
|
||||
}}</span
|
||||
>
|
||||
</li>
|
||||
|
|
@ -486,11 +481,15 @@
|
|||
<span
|
||||
>Hosting:
|
||||
{{
|
||||
hostingOptions.find((o) => o.value === constraints.hosting)?.label
|
||||
hostingOptions.find(
|
||||
(o) => o.value === constraints.hosting
|
||||
)?.label
|
||||
}}</span
|
||||
>
|
||||
</li>
|
||||
<li v-if="constraints.integrations.length > 0" class="flex items-start">
|
||||
<li
|
||||
v-if="constraints.integrations.length > 0"
|
||||
class="flex items-start">
|
||||
<span class="text-purple-600 mr-2">→</span>
|
||||
<span
|
||||
>Required Integrations:
|
||||
|
|
@ -502,7 +501,9 @@
|
|||
<span
|
||||
>Support Level:
|
||||
{{
|
||||
supportOptions.find((o) => o.value === constraints.support)?.label
|
||||
supportOptions.find(
|
||||
(o) => o.value === constraints.support
|
||||
)?.label
|
||||
}}</span
|
||||
>
|
||||
</li>
|
||||
|
|
@ -511,8 +512,9 @@
|
|||
<span
|
||||
>Migration Timeline:
|
||||
{{
|
||||
timelineOptions.find((o) => o.value === constraints.timeline)
|
||||
?.label
|
||||
timelineOptions.find(
|
||||
(o) => o.value === constraints.timeline
|
||||
)?.label
|
||||
}}</span
|
||||
>
|
||||
</li>
|
||||
|
|
@ -520,27 +522,27 @@
|
|||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h3 class="text-xl font-bold text-neutral-800 mb-3">Evaluation Rubric</h3>
|
||||
<h3 class="text-xl font-bold text-neutral-800 mb-3">
|
||||
Evaluation Rubric
|
||||
</h3>
|
||||
<p class="text-neutral-700 mb-4">
|
||||
Score each vendor option using these weighted criteria (0-5 scale):
|
||||
Score each vendor option using these weighted criteria (0-5
|
||||
scale):
|
||||
</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-neutral-100">
|
||||
<th
|
||||
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-left"
|
||||
>
|
||||
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-left">
|
||||
Criterion
|
||||
</th>
|
||||
<th
|
||||
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-left"
|
||||
>
|
||||
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-left">
|
||||
Description
|
||||
</th>
|
||||
<th
|
||||
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-center"
|
||||
>
|
||||
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-center">
|
||||
Weight
|
||||
</th>
|
||||
</tr>
|
||||
|
|
@ -549,21 +551,17 @@
|
|||
<tr
|
||||
v-for="weight in sortedWeights"
|
||||
:key="weight.id"
|
||||
class="hover:bg-neutral-50"
|
||||
>
|
||||
class="hover:bg-neutral-50">
|
||||
<td
|
||||
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 font-semibold"
|
||||
>
|
||||
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 font-semibold">
|
||||
{{ weight.name }}
|
||||
</td>
|
||||
<td
|
||||
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-sm text-neutral-600"
|
||||
>
|
||||
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-sm text-neutral-600">
|
||||
{{ weight.rubricDescription }}
|
||||
</td>
|
||||
<td
|
||||
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-center font-bold text-neutral-600"
|
||||
>
|
||||
class="border-2 border-neutral-900 dark:border-neutral-100 p-3 text-center font-bold text-neutral-600">
|
||||
{{ principleWeights[weight.id] }}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -580,8 +578,8 @@
|
|||
<li class="flex items-start">
|
||||
<span class="text-neutral-600 mr-2">→</span>
|
||||
<span
|
||||
>Any vendor failing a non-negotiable requirement is automatically
|
||||
eliminated</span
|
||||
>Any vendor failing a non-negotiable requirement is
|
||||
automatically eliminated</span
|
||||
>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
|
|
@ -594,8 +592,8 @@
|
|||
<li class="flex items-start">
|
||||
<span class="text-neutral-600 mr-2">→</span>
|
||||
<span
|
||||
>When scores are within 10%, choose based on alignment with
|
||||
cooperative values</span
|
||||
>When scores are within 10%, choose based on alignment
|
||||
with cooperative values</span
|
||||
>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
|
|
@ -638,11 +636,16 @@
|
|||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="text-neutral-600 mr-2">→</span>
|
||||
<span>Document any exceptions with clear justification</span>
|
||||
<span
|
||||
>Document any exceptions with clear justification</span
|
||||
>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="text-neutral-600 mr-2">→</span>
|
||||
<span>Share learnings with other cooperatives in our network</span>
|
||||
<span
|
||||
>Share learnings with other cooperatives in our
|
||||
network</span
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
|
@ -656,8 +659,7 @@
|
|||
<ExportOptions
|
||||
:export-data="exportData"
|
||||
filename="tech-charter"
|
||||
title="Technology Charter"
|
||||
/>
|
||||
title="Technology Charter" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -705,13 +707,15 @@ const principles = [
|
|||
id: "portability",
|
||||
name: "Data Freedom",
|
||||
description: "Easy export, no vendor lock-in, migration-friendly",
|
||||
rubricDescription: "Export capabilities, proprietary formats, switching costs",
|
||||
rubricDescription:
|
||||
"Export capabilities, proprietary formats, switching costs",
|
||||
defaultWeight: 4,
|
||||
},
|
||||
{
|
||||
id: "opensource",
|
||||
name: "Open Source & Community",
|
||||
description: "FOSS preference, transparent development, community governance",
|
||||
description:
|
||||
"FOSS preference, transparent development, community governance",
|
||||
rubricDescription: "License type, community involvement, code transparency",
|
||||
defaultWeight: 3,
|
||||
},
|
||||
|
|
@ -719,7 +723,8 @@ const principles = [
|
|||
id: "sustainability",
|
||||
name: "Sustainable Operations",
|
||||
description: "Predictable costs, green hosting, efficient resource use",
|
||||
rubricDescription: "Total cost of ownership, carbon footprint, resource efficiency",
|
||||
rubricDescription:
|
||||
"Total cost of ownership, carbon footprint, resource efficiency",
|
||||
defaultWeight: 3,
|
||||
},
|
||||
{
|
||||
|
|
@ -732,8 +737,10 @@ const principles = [
|
|||
{
|
||||
id: "usability",
|
||||
name: "User Experience",
|
||||
description: "Intuitive interface, minimal learning curve, daily efficiency",
|
||||
rubricDescription: "Onboarding time, user satisfaction, workflow integration",
|
||||
description:
|
||||
"Intuitive interface, minimal learning curve, daily efficiency",
|
||||
rubricDescription:
|
||||
"Onboarding time, user satisfaction, workflow integration",
|
||||
defaultWeight: 3,
|
||||
},
|
||||
];
|
||||
|
|
@ -769,7 +776,9 @@ const timelineOptions = [
|
|||
const sortedWeights = computed(() => {
|
||||
return principles
|
||||
.filter((p) => principleWeights.value[p.id] > 0)
|
||||
.sort((a, b) => principleWeights.value[b.id] - principleWeights.value[a.id]);
|
||||
.sort(
|
||||
(a, b) => principleWeights.value[b.id] - principleWeights.value[a.id]
|
||||
);
|
||||
});
|
||||
|
||||
const canGenerateCharter = computed(() => {
|
||||
|
|
@ -862,7 +871,9 @@ const resetForm = () => {
|
|||
};
|
||||
|
||||
const scrollToTop = () => {
|
||||
document.querySelector(".template-wrapper").scrollIntoView({ behavior: "smooth" });
|
||||
document
|
||||
.querySelector(".template-wrapper")
|
||||
.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
// Load saved data
|
||||
|
|
@ -905,9 +916,13 @@ onMounted(() => {
|
|||
});
|
||||
|
||||
// Auto-save when data changes
|
||||
watch([charterPurpose, principleWeights, nonNegotiables, constraints], autoSave, {
|
||||
deep: true,
|
||||
});
|
||||
watch(
|
||||
[charterPurpose, principleWeights, nonNegotiables, constraints],
|
||||
autoSave,
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -919,115 +934,6 @@ watch([charterPurpose, principleWeights, nonNegotiables, constraints], autoSave,
|
|||
@apply mb-8 relative;
|
||||
}
|
||||
|
||||
/* Principle card selected styling - using dithered shadow and background */
|
||||
.principle-selected {
|
||||
position: relative;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.principle-selected::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent 0px,
|
||||
transparent 1px,
|
||||
black 1px,
|
||||
black 2px
|
||||
);
|
||||
opacity: 0.1;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.principle-selected > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
html.dark .principle-selected {
|
||||
background: #0a0a0a;
|
||||
}
|
||||
|
||||
html.dark .principle-selected::after {
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent 0px,
|
||||
transparent 1px,
|
||||
white 1px,
|
||||
white 2px
|
||||
);
|
||||
}
|
||||
|
||||
/* Text background for better readability */
|
||||
.principle-label-bg {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.principle-label-bg.selected {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
/* Dark mode text backgrounds */
|
||||
html.dark .principle-text-bg {
|
||||
background: rgba(10, 10, 10, 0.9);
|
||||
}
|
||||
|
||||
html.dark .principle-text-bg.selected {
|
||||
background: rgba(10, 10, 10, 0.95);
|
||||
}
|
||||
|
||||
html.dark .principle-label-bg {
|
||||
background: rgba(10, 10, 10, 0.85);
|
||||
}
|
||||
|
||||
html.dark .principle-label-bg.selected {
|
||||
background: rgba(10, 10, 10, 0.95);
|
||||
}
|
||||
|
||||
/* Constraint button selected styling - black background */
|
||||
button.constraint-selected {
|
||||
background: black !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
button.constraint-selected:hover {
|
||||
background: black !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
html.dark button.constraint-selected {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
html.dark button.constraint-selected:hover {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.content-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
|
|
|
|||
372
pages/wizard.vue
372
pages/wizard.vue
|
|
@ -1,372 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- Wizard Subnav -->
|
||||
<WizardSubnav />
|
||||
|
||||
<section class="py-8 max-w-4xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="mb-10">
|
||||
<h1 class="text-5xl font-black text-black mb-4 leading-tight">
|
||||
Set up your co-op
|
||||
</h1>
|
||||
<p class="text-xl text-neutral-700 font-medium">
|
||||
Get your worker-owned co-op configured in a few simple steps. Jump to
|
||||
any step or work through them in order.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Completed State -->
|
||||
<div v-if="isCompleted" class="text-center py-12">
|
||||
<div
|
||||
class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<UIcon name="i-heroicons-check" class="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-black mb-2">You're all set!</h2>
|
||||
<p class="text-neutral-600 mb-6">
|
||||
Your co-op is configured and ready to go.
|
||||
</p>
|
||||
|
||||
<div class="flex justify-center gap-4">
|
||||
<UButton
|
||||
variant="outline"
|
||||
color="gray"
|
||||
@click="restartWizard"
|
||||
:disabled="isResetting">
|
||||
Start Over
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="navigateTo('/scenarios')"
|
||||
size="lg"
|
||||
variant="solid"
|
||||
color="black">
|
||||
Go to Dashboard
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vertical Steps Layout -->
|
||||
<div v-else class="space-y-4">
|
||||
<!-- Step 1: Members -->
|
||||
<div
|
||||
class="bg-white border-4 border-black rounded-xl overflow-hidden shadow-lg">
|
||||
<div
|
||||
class="p-8 cursor-pointer hover:bg-yellow-50 transition-colors"
|
||||
:class="{ 'bg-yellow-100': focusedStep === 1 }"
|
||||
@click="setFocusedStep(1)">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold"
|
||||
:class="
|
||||
membersStore.isValid
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-white text-black border-2 border-black'
|
||||
">
|
||||
<UIcon
|
||||
v-if="membersStore.isValid"
|
||||
name="i-heroicons-check"
|
||||
class="w-4 h-4" />
|
||||
<span v-else>1</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-2xl font-black text-black">Add your team</h3>
|
||||
</div>
|
||||
</div>
|
||||
<UIcon
|
||||
name="i-heroicons-chevron-down"
|
||||
class="w-6 h-6 text-black transition-transform font-bold"
|
||||
:class="{ 'rotate-180': focusedStep === 1 }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="focusedStep === 1" class="p-8 bg-yellow-25">
|
||||
<WizardMembersStep @save-status="handleSaveStatus" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Wage -->
|
||||
<div
|
||||
class="bg-white border-4 border-black rounded-xl overflow-hidden shadow-lg">
|
||||
<div
|
||||
class="p-8 cursor-pointer hover:bg-green-50 transition-colors"
|
||||
:class="{ 'bg-green-100': focusedStep === 2 }"
|
||||
@click="setFocusedStep(2)">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold"
|
||||
:class="
|
||||
policiesStore.isValid
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-white text-black border-2 border-black'
|
||||
">
|
||||
<UIcon
|
||||
v-if="policiesStore.isValid"
|
||||
name="i-heroicons-check"
|
||||
class="w-4 h-4" />
|
||||
<span v-else>2</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-2xl font-black text-black">Set your wage</h3>
|
||||
</div>
|
||||
</div>
|
||||
<UIcon
|
||||
name="i-heroicons-chevron-down"
|
||||
class="w-6 h-6 text-black transition-transform font-bold"
|
||||
:class="{ 'rotate-180': focusedStep === 2 }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="focusedStep === 2" class="p-8 bg-green-25">
|
||||
<WizardPoliciesStep @save-status="handleSaveStatus" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Costs -->
|
||||
<div
|
||||
class="bg-white border-4 border-black rounded-xl overflow-hidden shadow-lg">
|
||||
<div
|
||||
class="p-8 cursor-pointer hover:bg-blue-50 transition-colors"
|
||||
:class="{ 'bg-blue-100': focusedStep === 3 }"
|
||||
@click="setFocusedStep(3)">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full bg-green-100 text-green-700 flex items-center justify-center text-sm font-bold">
|
||||
<UIcon name="i-heroicons-check" class="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-2xl font-black text-black">Monthly costs</h3>
|
||||
</div>
|
||||
</div>
|
||||
<UIcon
|
||||
name="i-heroicons-chevron-down"
|
||||
class="w-6 h-6 text-black transition-transform font-bold"
|
||||
:class="{ 'rotate-180': focusedStep === 3 }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="focusedStep === 3" class="p-8 bg-blue-25">
|
||||
<WizardCostsStep @save-status="handleSaveStatus" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Revenue -->
|
||||
<div
|
||||
class="bg-white border-4 border-black rounded-xl overflow-hidden shadow-lg">
|
||||
<div
|
||||
class="p-8 cursor-pointer hover:bg-purple-50 transition-colors"
|
||||
:class="{ 'bg-purple-100': focusedStep === 4 }"
|
||||
@click="setFocusedStep(4)">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold"
|
||||
:class="
|
||||
streamsStore.hasValidStreams
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-white text-black border-2 border-black'
|
||||
">
|
||||
<UIcon
|
||||
v-if="streamsStore.hasValidStreams"
|
||||
name="i-heroicons-check"
|
||||
class="w-4 h-4" />
|
||||
<span v-else>4</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-2xl font-black text-black">
|
||||
Revenue streams
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<UIcon
|
||||
name="i-heroicons-chevron-down"
|
||||
class="w-6 h-6 text-black transition-transform font-bold"
|
||||
:class="{ 'rotate-180': focusedStep === 4 }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="focusedStep === 4" class="p-8 bg-purple-25">
|
||||
<WizardRevenueStep @save-status="handleSaveStatus" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 5: Review -->
|
||||
<div
|
||||
class="bg-white border-4 border-black rounded-xl overflow-hidden shadow-lg">
|
||||
<div
|
||||
class="p-8 cursor-pointer hover:bg-orange-50 transition-colors"
|
||||
:class="{ 'bg-orange-100': focusedStep === 5 }"
|
||||
@click="setFocusedStep(5)">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold"
|
||||
:class="
|
||||
canComplete
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-white text-black border-2 border-black'
|
||||
">
|
||||
<UIcon
|
||||
v-if="canComplete"
|
||||
name="i-heroicons-check"
|
||||
class="w-4 h-4" />
|
||||
<span v-else>5</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-2xl font-black text-black">
|
||||
Review & finish
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<UIcon
|
||||
name="i-heroicons-chevron-down"
|
||||
class="w-6 h-6 text-black transition-transform font-bold"
|
||||
:class="{ 'rotate-180': focusedStep === 5 }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="focusedStep === 5" class="p-8 bg-orange-25">
|
||||
<WizardReviewStep @complete="completeWizard" @reset="resetWizard" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Actions -->
|
||||
<div class="flex justify-between items-center pt-8">
|
||||
<UButton
|
||||
variant="outline"
|
||||
color="red"
|
||||
@click="resetWizard"
|
||||
:disabled="isResetting">
|
||||
Start Over
|
||||
</UButton>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Save status -->
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<UIcon
|
||||
v-if="saveStatus === 'saving'"
|
||||
name="i-heroicons-arrow-path"
|
||||
class="w-4 h-4 animate-spin text-neutral-500" />
|
||||
<UIcon
|
||||
v-if="saveStatus === 'saved'"
|
||||
name="i-heroicons-check-circle"
|
||||
class="w-4 h-4 text-green-500" />
|
||||
<span v-if="saveStatus === 'saving'" class="text-neutral-500"
|
||||
>Saving...</span
|
||||
>
|
||||
<span v-if="saveStatus === 'saved'" class="text-green-600"
|
||||
>Saved</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
v-if="canComplete"
|
||||
@click="completeWizard"
|
||||
size="lg"
|
||||
variant="solid"
|
||||
color="black">
|
||||
Complete Setup
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Stores
|
||||
const membersStore = useMembersStore();
|
||||
const policiesStore = usePoliciesStore();
|
||||
const streamsStore = useStreamsStore();
|
||||
const budgetStore = useBudgetStore();
|
||||
const wizardStore = useWizardStore();
|
||||
|
||||
// UI state
|
||||
const focusedStep = ref(1);
|
||||
const saveStatus = ref("");
|
||||
const isResetting = ref(false);
|
||||
const isCompleted = ref(false);
|
||||
|
||||
// Computed validation
|
||||
const canComplete = computed(
|
||||
() =>
|
||||
membersStore.isValid &&
|
||||
policiesStore.isValid &&
|
||||
streamsStore.hasValidStreams
|
||||
);
|
||||
|
||||
// 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) {
|
||||
// Toggle if clicking on already focused step
|
||||
if (focusedStep.value === step) {
|
||||
focusedStep.value = 0; // Close the section
|
||||
} else {
|
||||
focusedStep.value = step; // Open the section
|
||||
}
|
||||
}
|
||||
|
||||
function completeWizard() {
|
||||
// Mark setup as complete and show restart button for testing
|
||||
isCompleted.value = true;
|
||||
}
|
||||
|
||||
async function resetWizard() {
|
||||
isResetting.value = true;
|
||||
|
||||
// Reset all stores
|
||||
membersStore.resetMembers();
|
||||
policiesStore.resetPolicies();
|
||||
streamsStore.resetStreams();
|
||||
budgetStore.resetBudgetOverhead();
|
||||
|
||||
// Reset wizard state
|
||||
wizardStore.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 all stores and wizard state
|
||||
membersStore.resetMembers();
|
||||
policiesStore.resetPolicies();
|
||||
streamsStore.resetStreams();
|
||||
budgetStore.resetBudgetOverhead();
|
||||
wizardStore.reset();
|
||||
saveStatus.value = "";
|
||||
|
||||
// Small delay for UX
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
isResetting.value = false;
|
||||
}
|
||||
|
||||
// SEO
|
||||
useSeoMeta({
|
||||
title: "Setup Wizard - Configure Your Co-op",
|
||||
description:
|
||||
"Set up your co-op members, policies, costs, and revenue streams.",
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,11 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- Wizard Subnav -->
|
||||
<WizardSubnav />
|
||||
|
||||
<div
|
||||
class="template-container min-h-screen bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100 py-8"
|
||||
style="font-family: 'Ubuntu', 'Ubuntu Mono', monospace"
|
||||
>
|
||||
<div class="max-w-6xl mx-auto px-4 relative">
|
||||
<div class="mb-8">
|
||||
|
|
@ -72,69 +68,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 help-section">
|
||||
<div class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
||||
<div
|
||||
class="relative bg-white dark:bg-neutral-950 border border-black dark:border-white p-6"
|
||||
>
|
||||
<h2
|
||||
class="text-xl font-semibold text-neutral-900 dark:text-white mb-3"
|
||||
style="font-family: 'Ubuntu', monospace"
|
||||
>
|
||||
How Wizards Work
|
||||
</h2>
|
||||
<div class="grid md:grid-cols-2 gap-6 text-neutral-900 dark:text-neutral-100">
|
||||
<div>
|
||||
<h3
|
||||
class="font-medium mb-2 text-neutral-900 dark:text-white"
|
||||
style="font-family: 'Ubuntu Mono', monospace"
|
||||
>
|
||||
FILL OUT FORMS
|
||||
</h3>
|
||||
<p class="text-sm text-neutral-700 dark:text-neutral-200">
|
||||
Wizards include form fields for all necessary information. Data
|
||||
auto-saves as you type.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
class="font-medium mb-2 text-neutral-900 dark:text-white"
|
||||
style="font-family: 'Ubuntu Mono', monospace"
|
||||
>
|
||||
LOCAL STORAGE
|
||||
</h3>
|
||||
<p class="text-sm text-neutral-700 dark:text-neutral-200">
|
||||
All data saves in your browser only. Nothing is sent to external
|
||||
servers.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
class="font-medium mb-2 text-neutral-900 dark:text-white"
|
||||
style="font-family: 'Ubuntu Mono', monospace"
|
||||
>
|
||||
EXPORT OPTIONS
|
||||
</h3>
|
||||
<p class="text-sm text-neutral-700 dark:text-neutral-200">
|
||||
Download as PDF, plain text, Markdown.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
class="font-medium mb-2 text-neutral-900 dark:text-white"
|
||||
style="font-family: 'Ubuntu Mono', monospace"
|
||||
>
|
||||
RESUME ANYTIME
|
||||
</h3>
|
||||
<p class="text-sm text-neutral-700 dark:text-neutral-200">
|
||||
Come back later and your progress will be saved. Clear browser data to
|
||||
start fresh.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -158,7 +91,7 @@ const templates = [
|
|||
},
|
||||
{
|
||||
id: "conflict-resolution-framework",
|
||||
name: "Conflict Resolution Framework",
|
||||
name: "Conflict Resolution",
|
||||
description:
|
||||
"A customizable framework for handling conflicts with restorative justice principles, clear processes, and organizational values alignment.",
|
||||
icon: "i-heroicons-scale",
|
||||
|
|
@ -310,35 +243,4 @@ useHead({
|
|||
background: white;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.disabled-button {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.template-card > *,
|
||||
.help-section > *,
|
||||
button,
|
||||
.px-4,
|
||||
div[class*="border"] {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
* {
|
||||
border-radius: 0 !important;
|
||||
font-family: "Ubuntu", monospace;
|
||||
}
|
||||
|
||||
html.dark :deep(.text-neutral-700),
|
||||
html.dark :deep(.text-neutral-500),
|
||||
html.dark :deep(.bg-neutral-50),
|
||||
html.dark :deep(.bg-neutral-100) {
|
||||
color: white !important;
|
||||
background-color: #0a0a0a !important;
|
||||
}
|
||||
|
||||
:deep(.border-neutral-200),
|
||||
:deep(.border-neutral-300) {
|
||||
border-color: black !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue