refactor: update app.vue and various components to enhance UI consistency, replace color classes for improved accessibility, and refine layout for better user experience
This commit is contained in:
parent
7b4fb6c2fd
commit
24e8b7a3a8
41 changed files with 2395 additions and 1603 deletions
BIN
pages/.DS_Store
vendored
BIN
pages/.DS_Store
vendored
Binary file not shown.
128
pages/budget.vue
128
pages/budget.vue
|
|
@ -11,7 +11,7 @@
|
|||
'px-4 py-2 font-medium transition-none',
|
||||
activeView === 'monthly'
|
||||
? 'bg-black text-white'
|
||||
: 'bg-white text-black hover:bg-zinc-100',
|
||||
: 'bg-white text-black hover:bg-neutral-100',
|
||||
]">
|
||||
Monthly
|
||||
</button>
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
'px-4 py-2 font-medium border-l-2 border-black transition-none',
|
||||
activeView === 'annual'
|
||||
? 'bg-black text-white'
|
||||
: 'bg-white text-black hover:bg-zinc-100',
|
||||
: 'bg-white text-black hover:bg-neutral-100',
|
||||
]">
|
||||
Annual
|
||||
</button>
|
||||
|
|
@ -48,14 +48,14 @@
|
|||
<div class="max-w-md mx-auto space-y-6">
|
||||
<div class="text-6xl">📊</div>
|
||||
<h3 class="text-xl font-bold text-black">No budget data found</h3>
|
||||
<p class="text-gray-600">
|
||||
<p class="text-neutral-600">
|
||||
Your budget is empty. Complete the setup wizard to add your revenue
|
||||
streams, team members, and expenses.
|
||||
</p>
|
||||
<div class="flex justify-center">
|
||||
<NuxtLink
|
||||
to="/coop-builder"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-black hover:bg-zinc-800 border-2 border-black transition-colors">
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-black hover:bg-neutral-800 border-2 border-black transition-colors">
|
||||
Complete Setup Wizard
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
<th
|
||||
v-for="month in monthlyHeaders"
|
||||
:key="month.key"
|
||||
class="border-r border-gray-400 px-2 py-3 text-center font-medium min-w-[80px] last:border-r-0">
|
||||
class="border-r border-neutral-400 px-2 py-3 text-center font-medium min-w-[80px] last:border-r-0">
|
||||
{{ month.label }}
|
||||
</th>
|
||||
</tr>
|
||||
|
|
@ -94,7 +94,7 @@
|
|||
@click="showAddRevenueModal = true"
|
||||
size="xs"
|
||||
:ui="{
|
||||
base: 'bg-white text-black hover:bg-zinc-200 transition-none',
|
||||
base: 'bg-white text-black hover:bg-neutral-200 transition-none',
|
||||
}">
|
||||
+ Add
|
||||
</UButton>
|
||||
|
|
@ -107,9 +107,9 @@
|
|||
<template
|
||||
v-for="(items, categoryName) in groupedRevenue"
|
||||
:key="`revenue-${categoryName}`">
|
||||
<tr v-if="items.length > 0" class="border-t border-gray-300">
|
||||
<tr v-if="items.length > 0" class="border-t border-neutral-300">
|
||||
<td
|
||||
class="px-4 py-1 font-semibold sticky left-0 bg-zinc-100 z-10 border-black"
|
||||
class="px-4 py-1 font-semibold sticky left-0 bg-neutral-100 z-10 border-black"
|
||||
:colspan="monthlyHeaders.length + 1">
|
||||
{{ categoryName }}
|
||||
</td>
|
||||
|
|
@ -118,17 +118,17 @@
|
|||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'border-t border-gray-200 hover:bg-zinc-50 transition-all duration-300',
|
||||
'border-t border-neutral-200 hover:bg-neutral-50 transition-all duration-300',
|
||||
highlightedItemId === item.id &&
|
||||
'bg-yellow-100 animate-pulse',
|
||||
]">
|
||||
<td
|
||||
class="border-r-1 border-zinc-200 px-4 py-2 sticky left-0 bg-white z-10">
|
||||
class="border-r-1 border-neutral-200 px-4 py-2 sticky left-0 bg-white z-10">
|
||||
<div class="flex items-center justify-between group">
|
||||
<div class="flex-1">
|
||||
<div class="text-left w-full">
|
||||
<div class="font-medium">{{ item.name }}</div>
|
||||
<div class="text-xs text-gray-600">
|
||||
<div class="text-xs text-neutral-600">
|
||||
{{ item.subcategory }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -147,7 +147,7 @@
|
|||
<td
|
||||
v-for="month in monthlyHeaders"
|
||||
:key="month.key"
|
||||
class="border-r border-gray-200 px-1 py-1 last:border-r-0">
|
||||
class="border-r border-neutral-200 px-1 py-1 last:border-r-0">
|
||||
<input
|
||||
type="text"
|
||||
:value="formatValue(item.monthlyValues?.[month.key] || 0)"
|
||||
|
|
@ -157,9 +157,9 @@
|
|||
"
|
||||
@blur="handleBlur($event, 'revenue', item.id, month.key)"
|
||||
@keydown.enter="handleEnter($event)"
|
||||
class="w-full text-right px-1 py-0.5 border border-transparent hover:border-gray-400 focus:border-black focus:outline-none transition-none"
|
||||
class="w-full text-right px-1 py-0.5 border border-transparent hover:border-neutral-400 focus:border-black focus:outline-none transition-none"
|
||||
:class="{
|
||||
'bg-zinc-50': !item.monthlyValues?.[month.key],
|
||||
'bg-neutral-50': !item.monthlyValues?.[month.key],
|
||||
}" />
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -167,9 +167,9 @@
|
|||
|
||||
<!-- Total Revenue Row -->
|
||||
<tr
|
||||
class="border-t-1 border-black border-b-1 font-bold bg-zinc-100">
|
||||
class="border-t-1 border-black border-b-1 font-bold bg-neutral-100">
|
||||
<td
|
||||
class="border-r-1 border-black px-4 py-2 sticky left-0 bg-zinc-100 z-10">
|
||||
class="border-r-1 border-black px-4 py-2 sticky left-0 bg-neutral-100 z-10">
|
||||
TOTAL REVENUE
|
||||
</td>
|
||||
<td
|
||||
|
|
@ -189,7 +189,7 @@
|
|||
@click="showAddExpenseModal = true"
|
||||
size="xs"
|
||||
:ui="{
|
||||
base: 'bg-white text-black hover:bg-zinc-200 transition-none',
|
||||
base: 'bg-white text-black hover:bg-neutral-200 transition-none',
|
||||
}">
|
||||
+ Add
|
||||
</UButton>
|
||||
|
|
@ -202,9 +202,9 @@
|
|||
<template
|
||||
v-for="(items, categoryName) in groupedExpenses"
|
||||
:key="`expense-${categoryName}`">
|
||||
<tr v-if="items.length > 0" class="border-t border-gray-300">
|
||||
<tr v-if="items.length > 0" class="border-t border-neutral-300">
|
||||
<td
|
||||
class="px-4 py-1 font-semibold sticky left-0 bg-zinc-100 z-10"
|
||||
class="px-4 py-1 font-semibold sticky left-0 bg-neutral-100 z-10"
|
||||
:colspan="monthlyHeaders.length + 1">
|
||||
{{ categoryName }}
|
||||
</td>
|
||||
|
|
@ -213,12 +213,12 @@
|
|||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'border-t border-gray-200 hover:bg-zinc-50 transition-all duration-300',
|
||||
'border-t border-neutral-200 hover:bg-neutral-50 transition-all duration-300',
|
||||
highlightedItemId === item.id &&
|
||||
'bg-yellow-100 animate-pulse',
|
||||
]">
|
||||
<td
|
||||
class="border-r-1 border-zinc-200 px-4 py-2 sticky left-0 bg-white z-10">
|
||||
class="border-r-1 border-neutral-200 px-4 py-2 sticky left-0 bg-white z-10">
|
||||
<div class="flex items-center justify-between group">
|
||||
<div class="flex-1">
|
||||
<div class="text-left w-full">
|
||||
|
|
@ -236,7 +236,7 @@
|
|||
Auto
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600" v-if="!isPayrollItem(item.id)">
|
||||
<div class="text-xs text-neutral-600" v-if="!isPayrollItem(item.id)">
|
||||
{{ item.subcategory }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -258,7 +258,7 @@
|
|||
<td
|
||||
v-for="month in monthlyHeaders"
|
||||
:key="month.key"
|
||||
class="border-r border-gray-200 px-1 py-1 last:border-r-0">
|
||||
class="border-r border-neutral-200 px-1 py-1 last:border-r-0">
|
||||
<input
|
||||
type="text"
|
||||
:value="formatValue(item.monthlyValues?.[month.key] || 0)"
|
||||
|
|
@ -271,12 +271,12 @@
|
|||
@keydown.enter="handleEnter($event)"
|
||||
class="w-full text-right px-1 py-0.5 border-2 transition-none"
|
||||
:class="{
|
||||
'bg-zinc-50':
|
||||
'bg-neutral-50':
|
||||
!item.monthlyValues?.[month.key] &&
|
||||
!isPayrollItem(item.id),
|
||||
'bg-zinc-50 border-none cursor-not-allowed text-zinc-500':
|
||||
'bg-neutral-50 border-none cursor-not-allowed text-neutral-500':
|
||||
isPayrollItem(item.id),
|
||||
'border-transparent hover:border-zinc-400 focus:border-black focus:outline-none':
|
||||
'border-transparent hover:border-neutral-400 focus:border-black focus:outline-none':
|
||||
!isPayrollItem(item.id),
|
||||
}"
|
||||
:title="
|
||||
|
|
@ -289,15 +289,15 @@
|
|||
</template>
|
||||
|
||||
<!-- Total Expenses Row -->
|
||||
<tr class="border-t-1 border-black font-bold bg-zinc-100">
|
||||
<tr class="border-t-1 border-black font-bold bg-neutral-100">
|
||||
<td
|
||||
class="border-r-1 border-black px-4 py-2 sticky left-0 bg-zinc-100 z-10">
|
||||
class="border-r-1 border-black px-4 py-2 sticky left-0 bg-neutral-100 z-10">
|
||||
TOTAL EXPENSES
|
||||
</td>
|
||||
<td
|
||||
v-for="month in monthlyHeaders"
|
||||
:key="month.key"
|
||||
class="border-r border-gray-400 px-2 py-2 text-right last:border-r-0">
|
||||
class="border-r border-neutral-400 px-2 py-2 text-right last:border-r-0">
|
||||
{{ formatCurrency(monthlyTotals[month.key]?.expenses || 0) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -311,7 +311,7 @@
|
|||
<td
|
||||
v-for="month in monthlyHeaders"
|
||||
:key="month.key"
|
||||
class="border-r border-gray-400 px-2 py-3 text-right last:border-r-0"
|
||||
class="border-r border-neutral-400 px-2 py-3 text-right last:border-r-0"
|
||||
:class="
|
||||
getNetIncomeClass(monthlyTotals[month.key]?.net || 0)
|
||||
">
|
||||
|
|
@ -320,7 +320,7 @@
|
|||
</tr>
|
||||
|
||||
<!-- Cumulative Balance Row -->
|
||||
<tr class="border-t-1 border-gray-400 font-bold text-lg bg-blue-50">
|
||||
<tr class="border-t-1 border-neutral-400 font-bold text-lg bg-blue-50">
|
||||
<td
|
||||
class="border-r-1 border-black px-4 py-3 sticky left-0 bg-blue-50 z-10">
|
||||
CUMULATIVE BALANCE
|
||||
|
|
@ -328,7 +328,7 @@
|
|||
<td
|
||||
v-for="month in monthlyHeaders"
|
||||
:key="month.key"
|
||||
class="border-r border-gray-400 px-2 py-3 text-right last:border-r-0"
|
||||
class="border-r border-neutral-400 px-2 py-3 text-right last:border-r-0"
|
||||
:class="
|
||||
getCumulativeBalanceClass(cumulativeBalances[month.key] || 0)
|
||||
">
|
||||
|
|
@ -403,19 +403,19 @@
|
|||
placeholder="Enter annual amount (e.g., 12000)"
|
||||
size="lg">
|
||||
<template #leading>
|
||||
<span class="text-gray-500 font-medium">$</span>
|
||||
<span class="text-neutral-500 font-medium">$</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
<div class="bg-white rounded-lg p-4 border border-gray-200">
|
||||
<div class="bg-white rounded-lg p-4 border border-neutral-200">
|
||||
<div class="mb-2">
|
||||
<span class="text-sm font-medium text-gray-700"
|
||||
<span class="text-sm font-medium text-neutral-700"
|
||||
>Distribution Preview</span
|
||||
>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">
|
||||
<p class="text-sm text-neutral-600">
|
||||
This will divide
|
||||
<span class="font-semibold text-gray-900"
|
||||
<span class="font-semibold text-neutral-900"
|
||||
>${{ newRevenue.annualAmount || 0 }}</span
|
||||
>
|
||||
equally across all 12 months (<span
|
||||
|
|
@ -440,17 +440,17 @@
|
|||
placeholder="Enter monthly amount (e.g., 1000)"
|
||||
size="lg">
|
||||
<template #leading>
|
||||
<span class="text-gray-500 font-medium">$</span>
|
||||
<span class="text-neutral-500 font-medium">$</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
<div class="bg-white rounded-lg p-4 border border-gray-200">
|
||||
<div class="bg-white rounded-lg p-4 border border-neutral-200">
|
||||
<div class="mb-2">
|
||||
<span class="text-sm font-medium text-gray-700"
|
||||
<span class="text-sm font-medium text-neutral-700"
|
||||
>Monthly Preview</span
|
||||
>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">
|
||||
<p class="text-sm text-neutral-600">
|
||||
This will set
|
||||
<span class="font-semibold text-green-600"
|
||||
>${{ newRevenue.monthlyAmount || 0 }}</span
|
||||
|
|
@ -463,8 +463,8 @@
|
|||
<!-- Start Empty -->
|
||||
<div v-else>
|
||||
<div
|
||||
class="bg-white rounded-lg p-6 border border-gray-200 text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
class="bg-white rounded-lg p-6 border border-neutral-200 text-center">
|
||||
<p class="text-sm text-neutral-600">
|
||||
The revenue item will be created with no initial values. You
|
||||
can fill them in later directly in the budget table.
|
||||
</p>
|
||||
|
|
@ -545,19 +545,19 @@
|
|||
size="lg"
|
||||
class="text-sm font-medium w-full">
|
||||
<template #leading>
|
||||
<span class="text-gray-500 font-medium">$</span>
|
||||
<span class="text-neutral-500 font-medium">$</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
<div class="bg-white rounded-lg p-4 border border-gray-200">
|
||||
<div class="bg-white rounded-lg p-4 border border-neutral-200">
|
||||
<div class="mb-2">
|
||||
<span class="text-sm font-medium text-gray-700"
|
||||
<span class="text-sm font-medium text-neutral-700"
|
||||
>Distribution Preview</span
|
||||
>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">
|
||||
<p class="text-sm text-neutral-600">
|
||||
This will divide
|
||||
<span class="font-semibold text-gray-900"
|
||||
<span class="font-semibold text-neutral-900"
|
||||
>${{ newExpense.annualAmount || 0 }}</span
|
||||
>
|
||||
equally across all 12 months (<span
|
||||
|
|
@ -583,17 +583,17 @@
|
|||
size="lg"
|
||||
class="text-sm font-medium w-full">
|
||||
<template #leading>
|
||||
<span class="text-gray-500 font-medium">$</span>
|
||||
<span class="text-neutral-500 font-medium">$</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
<div class="bg-white rounded-lg p-4 border border-gray-200">
|
||||
<div class="bg-white rounded-lg p-4 border border-neutral-200">
|
||||
<div class="mb-2">
|
||||
<span class="text-sm font-medium text-gray-700"
|
||||
<span class="text-sm font-medium text-neutral-700"
|
||||
>Monthly Preview</span
|
||||
>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600">
|
||||
<p class="text-sm text-neutral-600">
|
||||
This will set
|
||||
<span class="font-semibold text-red-600"
|
||||
>${{ newExpense.monthlyAmount || 0 }}</span
|
||||
|
|
@ -606,8 +606,8 @@
|
|||
<!-- Start Empty -->
|
||||
<div v-else>
|
||||
<div
|
||||
class="bg-white rounded-lg p-6 border border-gray-200 text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
class="bg-white rounded-lg p-6 border border-neutral-200 text-center">
|
||||
<p class="text-sm text-neutral-600">
|
||||
The expense item will be created with no initial values. You
|
||||
can fill them in later directly in the budget table.
|
||||
</p>
|
||||
|
|
@ -650,8 +650,8 @@
|
|||
<!-- Revenue Section -->
|
||||
<div>
|
||||
<h4 class="font-semibold text-green-600 mb-2">📈 Revenue Calculation</h4>
|
||||
<p class="text-sm text-gray-600 mb-2">Revenue comes from your setup wizard streams and any manual additions:</p>
|
||||
<ul class="text-sm text-gray-600 space-y-1 ml-4">
|
||||
<p class="text-sm text-neutral-600 mb-2">Revenue comes from your setup wizard streams and any manual additions:</p>
|
||||
<ul class="text-sm text-neutral-600 space-y-1 ml-4">
|
||||
<li>• Monthly amounts you entered for each revenue stream</li>
|
||||
<li>• Varies by month based on your specific projections</li>
|
||||
</ul>
|
||||
|
|
@ -660,7 +660,7 @@
|
|||
<!-- Payroll Section -->
|
||||
<div>
|
||||
<h4 class="font-semibold text-blue-600 mb-2">👥 Smart Payroll Calculation</h4>
|
||||
<p class="text-sm text-gray-600 mb-2">Payroll uses a <strong>cumulative balance approach</strong> to ensure sustainability:</p>
|
||||
<p class="text-sm text-neutral-600 mb-2">Payroll uses a <strong>cumulative balance approach</strong> to ensure sustainability:</p>
|
||||
<div class="bg-blue-50 border border-blue-200 rounded p-3 text-sm">
|
||||
<p class="font-medium mb-2">Step-by-step process:</p>
|
||||
<ol class="space-y-1 ml-4">
|
||||
|
|
@ -671,7 +671,7 @@
|
|||
<li>5. Ensure cumulative balance doesn't fall below threshold</li>
|
||||
</ol>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 mt-2">
|
||||
<p class="text-sm text-neutral-600 mt-2">
|
||||
This means payroll varies by month - higher in good cash flow months, lower when cash is tight.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -679,8 +679,8 @@
|
|||
<!-- Cumulative Balance Section -->
|
||||
<div>
|
||||
<h4 class="font-semibold text-purple-600 mb-2">💰 Cumulative Balance</h4>
|
||||
<p class="text-sm text-gray-600 mb-2">Shows your running cash position over time:</p>
|
||||
<ul class="text-sm text-gray-600 space-y-1 ml-4">
|
||||
<p class="text-sm text-neutral-600 mb-2">Shows your running cash position over time:</p>
|
||||
<ul class="text-sm text-neutral-600 space-y-1 ml-4">
|
||||
<li>• Starts at $0 (current cash position)</li>
|
||||
<li>• Adds each month's net income (Revenue - All Expenses)</li>
|
||||
<li>• Helps you see when cash might run low</li>
|
||||
|
|
@ -691,7 +691,7 @@
|
|||
<!-- Policy Explanation -->
|
||||
<div>
|
||||
<h4 class="font-semibold text-orange-600 mb-2">⚖️ Pay Policy: {{ getPolicyName() }}</h4>
|
||||
<div class="text-sm text-gray-600">
|
||||
<div class="text-sm text-neutral-600">
|
||||
<p v-if="coopBuilderStore.policy?.relationship === 'equal-pay'">
|
||||
Everyone gets equal hourly wage (${{ coopBuilderStore.equalHourlyWage || 0 }}/hour) based on their monthly hours.
|
||||
</p>
|
||||
|
|
@ -704,8 +704,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 border border-gray-200 rounded p-3">
|
||||
<p class="text-sm text-gray-700">
|
||||
<div class="bg-neutral-50 border border-neutral-200 rounded p-3">
|
||||
<p class="text-sm text-neutral-700">
|
||||
<strong>Key insight:</strong> This system prioritizes sustainability over theoretical maximums.
|
||||
You might not always get full theoretical wages, but you'll never run out of cash.
|
||||
</p>
|
||||
|
|
@ -1231,7 +1231,7 @@ function formatCurrency(amount: number): string {
|
|||
function getNetIncomeClass(amount: number): string {
|
||||
if (amount > 0) return "text-green-600 font-bold";
|
||||
if (amount < 0) return "text-red-600 font-bold";
|
||||
return "text-gray-600";
|
||||
return "text-neutral-600";
|
||||
}
|
||||
|
||||
function getCumulativeBalanceClass(amount: number): string {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<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
|
||||
Budget Builder
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
|
||||
<div
|
||||
:class="[
|
||||
'relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-white overflow-hidden',
|
||||
'relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-neutral-500 overflow-hidden',
|
||||
focusedStep === 1 ? 'item-selected' : '',
|
||||
]">
|
||||
<div
|
||||
|
|
@ -94,7 +94,7 @@
|
|||
|
||||
<div
|
||||
v-if="focusedStep === 1"
|
||||
class="p-8 bg-neutral-50 dark:bg-neutral-900 border-t-2 border-black dark:border-white">
|
||||
class="p-8 bg-neutral-50 dark:bg-neutral-900 border-t-2 border-black dark:border-neutral-500">
|
||||
<WizardPoliciesStep @save-status="handleSaveStatus" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -109,7 +109,7 @@
|
|||
|
||||
<div
|
||||
:class="[
|
||||
'relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-white overflow-hidden',
|
||||
'relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-neutral-500 overflow-hidden',
|
||||
focusedStep === 2 ? 'item-selected' : '',
|
||||
]">
|
||||
<div
|
||||
|
|
@ -121,8 +121,8 @@
|
|||
class="w-8 h-8 flex items-center justify-center text-sm font-bold border-2"
|
||||
:class="
|
||||
membersValid
|
||||
? 'bg-black dark:bg-white text-white dark:text-black border-black dark:border-white'
|
||||
: 'bg-white dark:bg-neutral-950 text-black dark:text-white border-black dark:border-white'
|
||||
? 'bg-black dark:bg-white text-white dark:text-black border-black dark:border-neutral-500'
|
||||
: 'bg-white dark:bg-neutral-950 text-black dark:text-white border-black dark:border-neutral-500'
|
||||
">
|
||||
<UIcon
|
||||
v-if="membersValid"
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
|
||||
<div
|
||||
v-if="focusedStep === 2"
|
||||
class="p-8 bg-neutral-50 dark:bg-neutral-900 border-t-2 border-black dark:border-white">
|
||||
class="p-8 bg-neutral-50 dark:bg-neutral-900 border-t-2 border-black dark:border-neutral-500">
|
||||
<WizardMembersStep @save-status="handleSaveStatus" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -161,7 +161,7 @@
|
|||
|
||||
<div
|
||||
:class="[
|
||||
'relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-white overflow-hidden',
|
||||
'relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-neutral-500 overflow-hidden',
|
||||
focusedStep === 3 ? 'item-selected' : '',
|
||||
]">
|
||||
<div
|
||||
|
|
@ -173,8 +173,8 @@
|
|||
class="w-8 h-8 flex items-center justify-center text-sm font-bold border-2"
|
||||
:class="
|
||||
costsValid
|
||||
? 'bg-black dark:bg-white text-white dark:text-black border-black dark:border-white'
|
||||
: 'bg-white dark:bg-neutral-950 text-black dark:text-white border-black dark:border-white'
|
||||
? 'bg-black dark:bg-white text-white dark:text-black border-black dark:border-neutral-500'
|
||||
: 'bg-white dark:bg-neutral-950 text-black dark:text-white border-black dark:border-neutral-500'
|
||||
">
|
||||
<UIcon
|
||||
v-if="costsValid"
|
||||
|
|
@ -198,7 +198,7 @@
|
|||
|
||||
<div
|
||||
v-if="focusedStep === 3"
|
||||
class="p-8 bg-neutral-50 dark:bg-neutral-900 border-t-2 border-black dark:border-white">
|
||||
class="p-8 bg-neutral-50 dark:bg-neutral-900 border-t-2 border-black dark:border-neutral-500">
|
||||
<WizardCostsStep @save-status="handleSaveStatus" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -213,7 +213,7 @@
|
|||
|
||||
<div
|
||||
:class="[
|
||||
'relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-white overflow-hidden',
|
||||
'relative bg-white dark:bg-neutral-950 border-2 border-black dark:border-neutral-500 overflow-hidden',
|
||||
focusedStep === 4 ? 'item-selected' : '',
|
||||
]">
|
||||
<div
|
||||
|
|
@ -225,8 +225,8 @@
|
|||
class="w-8 h-8 flex items-center justify-center text-sm font-bold border-2"
|
||||
:class="
|
||||
streamsValid
|
||||
? 'bg-black dark:bg-white text-white dark:text-black border-black dark:border-white'
|
||||
: 'bg-white dark:bg-neutral-950 text-black dark:text-white border-black dark:border-white'
|
||||
? 'bg-black dark:bg-white text-white dark:text-black border-black dark:border-neutral-500'
|
||||
: 'bg-white dark:bg-neutral-950 text-black dark:text-white border-black dark:border-neutral-500'
|
||||
">
|
||||
<UIcon
|
||||
v-if="streamsValid"
|
||||
|
|
@ -250,7 +250,7 @@
|
|||
|
||||
<div
|
||||
v-if="focusedStep === 4"
|
||||
class="p-8 bg-neutral-50 dark:bg-neutral-900 border-t-2 border-black dark:border-white">
|
||||
class="p-8 bg-neutral-50 dark:bg-neutral-900 border-t-2 border-black dark:border-neutral-500">
|
||||
<WizardRevenueStep @save-status="handleSaveStatus" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -262,7 +262,7 @@
|
|||
class="export-btn"
|
||||
@click="resetWizard"
|
||||
:disabled="isResetting">
|
||||
Start Over
|
||||
Clear Data
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
|
|
@ -289,16 +289,6 @@
|
|||
>
|
||||
</div>
|
||||
|
||||
<!-- View Dashboard button (when partially complete) -->
|
||||
<button
|
||||
v-if="hasBasicData && !canComplete"
|
||||
class="export-btn"
|
||||
@click="navigateTo('/dashboard')"
|
||||
>
|
||||
<UIcon name="i-heroicons-chart-bar" class="mr-2" />
|
||||
View Dashboard
|
||||
</button>
|
||||
|
||||
<UTooltip :text="incompleteSectionsText" :prevent="canComplete">
|
||||
<button
|
||||
class="export-btn primary"
|
||||
|
|
@ -445,7 +435,7 @@ async function restartWizard() {
|
|||
|
||||
// SEO
|
||||
useSeoMeta({
|
||||
title: "Co-op Builder - Build Your Financial Foundation",
|
||||
title: "Budget Builder",
|
||||
description:
|
||||
"Build your co-op's financial foundation: set up members, policies, costs, and revenue streams.",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="space-y-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-semibold">Dashboard</h1>
|
||||
<div class="text-sm text-gray-600">
|
||||
<div class="text-sm text-neutral-600">
|
||||
Mode: {{ currentMode }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -15,15 +15,15 @@
|
|||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-green-600">{{ runwayDisplay }}</div>
|
||||
<div class="text-sm text-gray-600">Runway</div>
|
||||
<div class="text-sm text-neutral-600">Runway</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-blue-600">{{ coverageDisplay }}</div>
|
||||
<div class="text-sm text-gray-600">Coverage</div>
|
||||
<div class="text-sm text-neutral-600">Coverage</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-purple-600">{{ streamCount }}</div>
|
||||
<div class="text-sm text-gray-600">Revenue Streams</div>
|
||||
<div class="text-sm text-neutral-600">Revenue Streams</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
|
@ -34,11 +34,11 @@
|
|||
<h3 class="text-lg font-medium">Members ({{ memberCount }})</h3>
|
||||
</template>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(member, index) in membersList" :key="index" class="flex items-center justify-between p-2 border border-gray-200 rounded">
|
||||
<div v-for="(member, index) in membersList" :key="index" class="flex items-center justify-between p-2 border border-neutral-200 rounded">
|
||||
<span class="font-medium">{{ member.name }}</span>
|
||||
<span class="text-sm text-gray-600">{{ member.relationship }}</span>
|
||||
<span class="text-sm text-neutral-600">{{ member.relationship }}</span>
|
||||
</div>
|
||||
<div v-if="memberCount === 0" class="text-sm text-gray-500 italic p-4">
|
||||
<div v-if="memberCount === 0" class="text-sm text-neutral-500 italic p-4">
|
||||
No members configured yet.
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div class="min-h-screen bg-gray-50 py-8">
|
||||
<div class="min-h-screen bg-neutral-50 py-8">
|
||||
<div class="container mx-auto max-w-4xl px-4">
|
||||
<!-- Header -->
|
||||
<div class="mb-8 text-center">
|
||||
<h1 class="text-4xl font-bold mb-4">Budget Planning Help</h1>
|
||||
<p class="text-xl text-gray-600">Learn how to build a sustainable financial plan for your co-op or studio</p>
|
||||
<p class="text-xl text-neutral-600">Learn how to build a sustainable financial plan for your co-op or studio</p>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
|
|
@ -15,19 +15,19 @@
|
|||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<a href="#revenue-diversification" class="block p-3 bg-blue-50 border-2 border-blue-200 rounded hover:bg-blue-100 transition-colors">
|
||||
<span class="font-semibold">Revenue Diversification</span>
|
||||
<p class="text-sm text-gray-600">How to develop multiple income streams</p>
|
||||
<p class="text-sm text-neutral-600">How to develop multiple income streams</p>
|
||||
</a>
|
||||
<a href="#budget-categories" class="block p-3 bg-green-50 border-2 border-green-200 rounded hover:bg-green-100 transition-colors">
|
||||
<span class="font-semibold">Budget Categories</span>
|
||||
<p class="text-sm text-gray-600">Understanding revenue and expense types</p>
|
||||
<p class="text-sm text-neutral-600">Understanding revenue and expense types</p>
|
||||
</a>
|
||||
<a href="#planning-tips" class="block p-3 bg-yellow-50 border-2 border-yellow-200 rounded hover:bg-yellow-100 transition-colors">
|
||||
<span class="font-semibold">Planning Tips</span>
|
||||
<p class="text-sm text-gray-600">Best practices for financial planning</p>
|
||||
<p class="text-sm text-neutral-600">Best practices for financial planning</p>
|
||||
</a>
|
||||
<a href="#getting-started" class="block p-3 bg-purple-50 border-2 border-purple-200 rounded hover:bg-purple-100 transition-colors">
|
||||
<span class="font-semibold">Getting Started</span>
|
||||
<p class="text-sm text-gray-600">Step-by-step setup guide</p>
|
||||
<p class="text-sm text-neutral-600">Step-by-step setup guide</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="space-y-8">
|
||||
<div class="text-center">
|
||||
<h1 class="text-3xl font-bold mb-4">Project Budget Estimate</h1>
|
||||
<p class="text-gray-600 max-w-2xl mx-auto mb-4">
|
||||
<p class="text-neutral-600 max-w-2xl mx-auto mb-4">
|
||||
Get a quick estimate of what it would cost to build your project with fair pay.
|
||||
This tool helps worker co-ops sketch project budgets and break-even scenarios.
|
||||
</p>
|
||||
|
|
@ -18,10 +18,10 @@
|
|||
</div>
|
||||
|
||||
<div v-if="membersWithPay.length === 0" class="text-center py-8">
|
||||
<p class="text-gray-600 mb-4">No team members set up yet.</p>
|
||||
<p class="text-neutral-600 mb-4">No team members set up yet.</p>
|
||||
<NuxtLink
|
||||
to="/coop-builder"
|
||||
class="px-4 py-2 border-2 border-black bg-white font-bold hover:bg-gray-100"
|
||||
class="px-4 py-2 border-2 border-black bg-white font-bold hover:bg-neutral-100"
|
||||
>
|
||||
Set up your team in Setup Wizard
|
||||
</NuxtLink>
|
||||
|
|
|
|||
|
|
@ -21,52 +21,25 @@
|
|||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- Section 1: Organization Information -->
|
||||
<!-- Section 1: Cooperative Information -->
|
||||
<div class="section-card">
|
||||
<h2 class="section-title">1. Organization Information</h2>
|
||||
|
||||
<div class="space-y-6">
|
||||
<UFormField label="Organization Name" class="form-group-large">
|
||||
<UFormField label="Cooperative Name" class="form-group-large">
|
||||
<UInput
|
||||
v-model="formData.orgName"
|
||||
placeholder="Enter your organization name"
|
||||
placeholder="Enter your cooperative name"
|
||||
size="xl"
|
||||
class="w-full"
|
||||
:error="validationErrors.orgName"
|
||||
@input="debouncedAutoSave" />
|
||||
</UFormField>
|
||||
<div class="flex flex-row gap-4 space-x-4">
|
||||
<UFormField label="Organization Type" class="form-group-large">
|
||||
<USelect
|
||||
v-model="formData.orgType"
|
||||
:items="orgTypeOptions"
|
||||
placeholder="Select organization type..."
|
||||
size="xl"
|
||||
class="w-full"
|
||||
:error="validationErrors.orgType"
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Number of Members/Staff"
|
||||
class="form-group-large">
|
||||
<UInput
|
||||
v-model="formData.memberCount"
|
||||
type="number"
|
||||
min="2"
|
||||
placeholder="e.g., 5"
|
||||
size="xl"
|
||||
:error="validationErrors.memberCount"
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: Core Values -->
|
||||
<div class="section-card">
|
||||
<div class="flex flex-row justify-between items-center">
|
||||
<h2 class="section-title">2. Guiding Principles & Values</h2>
|
||||
<h2 class="section-title">2. Values</h2>
|
||||
<div class="flex flex-row gap-2 items-center no-print no-pdf">
|
||||
<USwitch
|
||||
v-model="sectionsEnabled.values"
|
||||
|
|
@ -80,7 +53,7 @@
|
|||
|
||||
<div class="space-y-6" v-show="sectionsEnabled.values">
|
||||
<UFormField
|
||||
label="Select Core Values (check all that apply)"
|
||||
label="Select core values (check all that apply)"
|
||||
class="form-group-large">
|
||||
<div class="values-grid">
|
||||
<div
|
||||
|
|
@ -110,12 +83,12 @@
|
|||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Additional Values or Principles"
|
||||
label="Additional values or principles"
|
||||
class="form-group-large">
|
||||
<UTextarea
|
||||
v-model="formData.customValues"
|
||||
:rows="3"
|
||||
placeholder="Add any additional values specific to your organization..."
|
||||
placeholder="Add any additional values specific to your cooperative..."
|
||||
size="xl"
|
||||
class="w-full"
|
||||
@input="debouncedAutoSave" />
|
||||
|
|
@ -230,13 +203,14 @@
|
|||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Mediator/Facilitator Structure"
|
||||
label="Mediator/facilitator structure"
|
||||
class="form-group-large">
|
||||
<USelect
|
||||
v-model="formData.mediatorType"
|
||||
:items="mediatorTypeOptions"
|
||||
placeholder="Select mediator structure..."
|
||||
size="xl"
|
||||
class="w-full"
|
||||
:error="validationErrors.mediatorType"
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
|
|
@ -381,7 +355,7 @@
|
|||
|
||||
<div class="space-y-6">
|
||||
<UFormField
|
||||
label="Available Actions (check all that apply)"
|
||||
label="Available actions (check all that apply)"
|
||||
class="form-group-large">
|
||||
<div class="checkbox-group mt-4 space-y-3">
|
||||
<div
|
||||
|
|
@ -484,7 +458,7 @@
|
|||
v-model="formData.training"
|
||||
:rows="3"
|
||||
class="w-full"
|
||||
placeholder="Describe any training needed for members, facilitators, or committee members..."
|
||||
placeholder="Describe any training needed for member-workers, facilitators, or committee members..."
|
||||
size="xl"
|
||||
@input="debouncedAutoSave" />
|
||||
</UFormField>
|
||||
|
|
@ -672,18 +646,18 @@
|
|||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Staff Liaison for Conflict Resolution Committee"
|
||||
label="Member Liaison for Conflict Resolution Committee"
|
||||
class="form-group-large">
|
||||
<UInput
|
||||
v-model="formData.staffLiaison"
|
||||
placeholder="Title/role of designated staff liaison"
|
||||
placeholder="Title/role of designated member liaison"
|
||||
size="xl"
|
||||
class="w-full md:w-1/2"
|
||||
@input="debouncedAutoSave" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Board Chair Role in Conflict Resolution"
|
||||
label="Elected Board Chair Role in Conflict Resolution"
|
||||
class="form-group-large">
|
||||
<USelect
|
||||
v-model="formData.boardChairRole"
|
||||
|
|
@ -762,7 +736,7 @@
|
|||
v-model="formData.requireExternalAdvice"
|
||||
id="require-external-advice"
|
||||
label="Require external legal advice for complex complaints"
|
||||
help="Seek external expertise for multi-party or staff/director complaints"
|
||||
help="Seek external expertise for multi-party or member-coordinator complaints"
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
|
@ -893,7 +867,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed, onMounted } from "vue";
|
||||
import { ref, watch, computed } from "vue";
|
||||
|
||||
// Import centralized coop info
|
||||
const { coopInfo, updateCoopInfo, getOrgName } = useCoopInfo();
|
||||
|
|
@ -913,21 +887,7 @@ useHead({
|
|||
],
|
||||
});
|
||||
|
||||
// Import PDF export composable
|
||||
const { exportToPDF } = usePdfExportBasic();
|
||||
|
||||
const showPreview = ref(false);
|
||||
const copySuccess = ref(false);
|
||||
|
||||
// Options for dropdowns (using simple string arrays like the working membership template)
|
||||
const orgTypeOptions = [
|
||||
"Worker Cooperative",
|
||||
"Consumer Cooperative",
|
||||
"Nonprofit",
|
||||
"Collective",
|
||||
"Community Group",
|
||||
"Other",
|
||||
];
|
||||
|
||||
const approachOptions = [
|
||||
{
|
||||
|
|
@ -1014,17 +974,17 @@ const reflectionPeriodOptions = [
|
|||
];
|
||||
|
||||
const internalAdvisorOptions = [
|
||||
"Single Board-appointed advisor",
|
||||
"Rotating Board members",
|
||||
"Single elected advisor",
|
||||
"Rotating member representatives",
|
||||
"External neutral advisor",
|
||||
"Committee-designated advisor",
|
||||
"Staff member with training",
|
||||
"Trained member facilitator",
|
||||
];
|
||||
|
||||
const boardChairRoleOptions = [
|
||||
"First contact for ED complaints",
|
||||
"First contact for coordinator complaints",
|
||||
"Appeals reviewer",
|
||||
"Final decision maker",
|
||||
"Participates in collective decision",
|
||||
"Advisory role only",
|
||||
"Not involved in conflicts",
|
||||
];
|
||||
|
|
@ -1089,21 +1049,23 @@ const coreValues = ref([
|
|||
]);
|
||||
|
||||
const conflictTypes = ref([
|
||||
{ label: "Interpersonal disputes between members", checked: true },
|
||||
{ label: "Interpersonal disputes between member-workers", checked: true },
|
||||
{ label: "Code of Conduct violations", checked: true },
|
||||
{ label: "Work allocation and responsibility disagreements", checked: true },
|
||||
{ label: "Decision-making process conflicts", checked: true },
|
||||
{ label: "Harassment or discrimination", checked: false },
|
||||
{ label: "Work performance issues", checked: false },
|
||||
{ label: "Conflicts of interest", checked: false },
|
||||
{ label: "Member-owner responsibility disputes", checked: false },
|
||||
{ label: "Collective ownership tensions", checked: false },
|
||||
{ label: "External organization disputes", checked: false },
|
||||
{ label: "Financial disagreements", checked: false },
|
||||
]);
|
||||
|
||||
const reportReceivers = ref([
|
||||
{ label: "Designated conflict resolution committee", checked: true },
|
||||
{ label: "Any board member", checked: false },
|
||||
{ label: "Executive Director(s)", checked: false },
|
||||
{ label: "Designated staff liaison", checked: false },
|
||||
{ label: "Any member", checked: false },
|
||||
{ label: "Any elected board member", checked: false },
|
||||
{ label: "Administrative Coordinator(s)", checked: false },
|
||||
{ label: "Designated member liaison", checked: false },
|
||||
{ label: "Any member-worker", checked: false },
|
||||
]);
|
||||
|
||||
const processSteps = ref([
|
||||
|
|
@ -1124,7 +1086,7 @@ const availableActions = ref([
|
|||
{ label: "Temporary suspension", checked: true },
|
||||
{ label: "Role/responsibility changes", checked: false },
|
||||
{ label: "Mediated agreement", checked: false },
|
||||
{ label: "Removal from organization", checked: true },
|
||||
{ label: "Removal from the cooperative", checked: true },
|
||||
{ label: "Restorative circle/process", checked: false },
|
||||
]);
|
||||
|
||||
|
|
@ -1168,7 +1130,7 @@ const formData = ref({
|
|||
documentDirectResolution: true,
|
||||
internalAdvisorType: "Single Board-appointed advisor",
|
||||
staffLiaison: "",
|
||||
boardChairRole: "First contact for ED complaints",
|
||||
boardChairRole: "First contact for coordinator complaints",
|
||||
formalAcknowledgmentTime: "Within 1 week",
|
||||
formalReviewTime: "1 month",
|
||||
requireExternalAdvice: true,
|
||||
|
|
@ -1183,165 +1145,12 @@ const formData = ref({
|
|||
// Validation logic
|
||||
const validationErrors = ref({});
|
||||
|
||||
const validateForm = () => {
|
||||
const errors = {};
|
||||
|
||||
// Required text fields
|
||||
if (!formData.value.orgName?.trim()) {
|
||||
errors.orgName = "Organization name is required";
|
||||
}
|
||||
if (!formData.value.orgType?.trim()) {
|
||||
errors.orgType = "Organization type is required";
|
||||
}
|
||||
if (!formData.value.memberCount?.toString().trim()) {
|
||||
errors.memberCount = "Number of members/staff is required";
|
||||
}
|
||||
if (!formData.value.approach?.trim()) {
|
||||
errors.approach = "Primary resolution approach is required";
|
||||
}
|
||||
if (!formData.value.mediatorType?.trim()) {
|
||||
errors.mediatorType = "Mediator/facilitator structure is required";
|
||||
}
|
||||
if (!formData.value.initialResponse?.trim()) {
|
||||
errors.initialResponse = "Initial response time is required";
|
||||
}
|
||||
if (!formData.value.resolutionTarget?.trim()) {
|
||||
errors.resolutionTarget = "Target resolution time is required";
|
||||
}
|
||||
if (!formData.value.reviewSchedule?.trim()) {
|
||||
errors.reviewSchedule = "Policy review schedule is required";
|
||||
}
|
||||
if (!formData.value.amendments?.trim()) {
|
||||
errors.amendments = "Amendment process is required";
|
||||
}
|
||||
|
||||
// Required checkbox groups (must have at least one checked)
|
||||
const checkedConflictTypes = conflictTypes.value.filter(
|
||||
(item) => item.checked
|
||||
);
|
||||
if (checkedConflictTypes.length === 0) {
|
||||
errors.conflictTypes = "Please select at least one type of conflict";
|
||||
}
|
||||
|
||||
// Note: Guiding Principles & Values section is optional - no validation needed
|
||||
|
||||
const checkedReportReceivers = reportReceivers.value.filter(
|
||||
(item) => item.checked
|
||||
);
|
||||
if (checkedReportReceivers.length === 0) {
|
||||
errors.reportReceivers = "Please select at least one report receiver";
|
||||
}
|
||||
|
||||
const checkedProcessSteps = processSteps.value.filter((item) => item.checked);
|
||||
if (checkedProcessSteps.length === 0) {
|
||||
errors.processSteps = "Please select at least one process step";
|
||||
}
|
||||
|
||||
const checkedAvailableActions = availableActions.value.filter(
|
||||
(item) => item.checked
|
||||
);
|
||||
if (checkedAvailableActions.length === 0) {
|
||||
errors.availableActions = "Please select at least one available action";
|
||||
}
|
||||
|
||||
// Note: Special circumstances section is optional - no validation needed
|
||||
|
||||
validationErrors.value = errors;
|
||||
const isValid = Object.keys(errors).length === 0;
|
||||
|
||||
// Provide user feedback
|
||||
if (isValid) {
|
||||
alert("✅ Form is complete and ready for export!");
|
||||
} else {
|
||||
const errorCount = Object.keys(errors).length;
|
||||
alert(
|
||||
`❌ Please complete ${errorCount} required field${
|
||||
errorCount > 1 ? "s" : ""
|
||||
} before exporting.`
|
||||
);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
// Completion percentage computation
|
||||
const completionPercentage = computed(() => {
|
||||
const allInputs = [
|
||||
formData.value.orgName,
|
||||
formData.value.orgType,
|
||||
formData.value.memberCount,
|
||||
formData.value.approach,
|
||||
formData.value.mediatorType,
|
||||
formData.value.initialResponse,
|
||||
formData.value.resolutionTarget,
|
||||
formData.value.reviewSchedule,
|
||||
formData.value.amendments,
|
||||
];
|
||||
|
||||
const checkboxInputs = [
|
||||
...coreValues.value,
|
||||
...conflictTypes.value,
|
||||
...reportReceivers.value,
|
||||
...processSteps.value,
|
||||
...availableActions.value,
|
||||
...specialCircumstances.value,
|
||||
];
|
||||
|
||||
const filledInputs = allInputs.filter(
|
||||
(val) => val && val.toString().trim() !== ""
|
||||
).length;
|
||||
const checkedBoxes = checkboxInputs.filter((item) => item.checked).length;
|
||||
|
||||
const totalFields = allInputs.length + checkboxInputs.length;
|
||||
const completedFields = filledInputs + checkedBoxes;
|
||||
|
||||
return Math.round((completedFields / totalFields) * 100);
|
||||
});
|
||||
|
||||
// Load saved data
|
||||
const loadSavedData = () => {
|
||||
if (process.client) {
|
||||
const saved = localStorage.getItem("conflict-resolution-framework-data");
|
||||
if (saved) {
|
||||
try {
|
||||
const parsedData = JSON.parse(saved);
|
||||
|
||||
// Load form data
|
||||
if (parsedData.formData) {
|
||||
formData.value = { ...formData.value, ...parsedData.formData };
|
||||
}
|
||||
|
||||
// Load checkbox arrays
|
||||
if (parsedData.coreValues) coreValues.value = parsedData.coreValues;
|
||||
if (parsedData.conflictTypes)
|
||||
conflictTypes.value = parsedData.conflictTypes;
|
||||
if (parsedData.reportReceivers)
|
||||
reportReceivers.value = parsedData.reportReceivers;
|
||||
if (parsedData.processSteps)
|
||||
processSteps.value = parsedData.processSteps;
|
||||
if (parsedData.availableActions)
|
||||
availableActions.value = parsedData.availableActions;
|
||||
if (parsedData.specialCircumstances)
|
||||
specialCircumstances.value = parsedData.specialCircumstances;
|
||||
if (parsedData.communicationChannels)
|
||||
communicationChannels.value = parsedData.communicationChannels;
|
||||
if (parsedData.formalComplaintElements)
|
||||
formalComplaintElements.value = parsedData.formalComplaintElements;
|
||||
if (parsedData.sectionsEnabled)
|
||||
sectionsEnabled.value = parsedData.sectionsEnabled;
|
||||
} catch (error) {
|
||||
console.error("Error loading saved data:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-save functionality
|
||||
const autoSave = () => {
|
||||
// Clear validation errors when users start correcting fields
|
||||
clearValidationErrors();
|
||||
|
||||
if (process.client) {
|
||||
if (typeof window !== "undefined") {
|
||||
const dataToSave = {
|
||||
formData: formData.value,
|
||||
coreValues: coreValues.value,
|
||||
|
|
@ -1383,7 +1192,7 @@ const markdownToHtml = (markdown) => {
|
|||
.replace(/<\/li>\s*<ul>/g, "</li>")
|
||||
.replace(/<\/ul>\s*<li>/g, "<li>")
|
||||
// Tables (basic support)
|
||||
.replace(/^\|(.+)\|$/gm, (match, content) => {
|
||||
.replace(/^\|(.+)\|$/gm, (_, content) => {
|
||||
const cells = content.split("|").map((cell) => cell.trim());
|
||||
if (cells.every((cell) => cell.match(/^-+$/))) {
|
||||
return ""; // Skip separator rows
|
||||
|
|
@ -1471,29 +1280,309 @@ watch(
|
|||
{ deep: true }
|
||||
);
|
||||
|
||||
// Export data for the ExportOptions component
|
||||
const exportData = computed(() => ({
|
||||
formData: formData.value,
|
||||
orgName: getOrgName(),
|
||||
orgType: formData.value.orgType,
|
||||
memberCount: formData.value.memberCount,
|
||||
sectionsEnabled: sectionsEnabled.value,
|
||||
coreValues: formData.value.coreValues,
|
||||
principles: formData.value.principles,
|
||||
policies: {
|
||||
memberInvolvement: formData.value.memberInvolvement,
|
||||
communicationGuidelines: formData.value.communicationGuidelines,
|
||||
processSteps: formData.value.processSteps,
|
||||
escalationCriteria: formData.value.escalationCriteria,
|
||||
mediation: formData.value.mediation,
|
||||
finalDecision: formData.value.finalDecision,
|
||||
learning: formData.value.learning,
|
||||
emergencyProcedures: formData.value.emergencyProcedures,
|
||||
annualReview: formData.value.annualReview,
|
||||
},
|
||||
exportedAt: new Date().toISOString(),
|
||||
section: "conflict-resolution-framework",
|
||||
}));
|
||||
// Generate the complete policy document for preview and export
|
||||
const generatePolicyDocument = () => {
|
||||
const cooperativeName = formData.value.orgName || "[Cooperative Name]";
|
||||
let content = `# ${cooperativeName} Conflict Resolution Policy\n\n`;
|
||||
|
||||
content += `*Framework Created: ${
|
||||
formData.value.createdDate || new Date().toISOString().split("T")[0]
|
||||
}*\n`;
|
||||
if (formData.value.reviewDate) {
|
||||
content += `*Next Review: ${formData.value.reviewDate}*\n`;
|
||||
}
|
||||
content += `\n---\n\n`;
|
||||
|
||||
// Core Values section (if enabled)
|
||||
if (sectionsEnabled.value.values) {
|
||||
content += `## Our Values\n\n`;
|
||||
content += `This conflict resolution framework is guided by our core values:\n\n`;
|
||||
|
||||
const selectedValues = coreValues.value.filter((v) => v.checked);
|
||||
if (selectedValues.length > 0) {
|
||||
selectedValues.forEach((value) => {
|
||||
content += `- **${value.label}**\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
if (formData.value.customValues) {
|
||||
content += `${formData.value.customValues}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolution Philosophy
|
||||
const approachDescriptions = {
|
||||
restorative:
|
||||
"We use a **restorative/loving justice** approach that focuses on healing, understanding root causes, and repairing relationships rather than punishment.",
|
||||
mediation:
|
||||
"We use a **mediation-first** approach where neutral third-party facilitators help parties dialogue and find solutions.",
|
||||
progressive:
|
||||
"We use **progressive discipline** with clear escalation steps and defined consequences for violations.",
|
||||
hybrid:
|
||||
"We use a **hybrid approach** that combines multiple methods based on the type and severity of conflict.",
|
||||
};
|
||||
|
||||
if (
|
||||
formData.value.approach &&
|
||||
approachDescriptions[formData.value.approach]
|
||||
) {
|
||||
content += `## Our Approach\n\n`;
|
||||
content += `${approachDescriptions[formData.value.approach]}\n\n`;
|
||||
content += `We do our best to resolve conflicts at the lowest possible escalation step (direct resolution), but agree to escalate conflicts (to assisted resolution) if they are not resolved.\n\n`;
|
||||
}
|
||||
|
||||
// Reflection Process (if enabled)
|
||||
if (sectionsEnabled.value.reflection) {
|
||||
content += `## Reflection\n\n`;
|
||||
content += `Before engaging in direct resolution, we encourage taking time for reflection:\n\n`;
|
||||
content += `1. **Set aside time to think** through what happened. What was the other person's behaviour? How did it affect you? *Distinguish other people's **actions** from your **feelings** about them.*\n`;
|
||||
content += `2. **Consider uncertainties** or misunderstandings that may have occurred.\n`;
|
||||
content += `3. **Distinguish disagreement from personal hostility.** Disagreement and dissent are part of healthy discussion. Hostility is not.\n`;
|
||||
content += `4. **Use your personal support system** (friends, family, therapist, etc.) to work through and clarify your perspective.\n`;
|
||||
content += `5. **Ask yourself** what part you played, how you could have behaved differently, and what your needs are.\n\n`;
|
||||
|
||||
if (formData.value.customReflectionPrompts) {
|
||||
content += `### Additional Reflection Prompts\n\n`;
|
||||
content += `${formData.value.customReflectionPrompts}\n\n`;
|
||||
}
|
||||
|
||||
const reflectionTiming =
|
||||
formData.value.reflectionPeriod || "Before any escalation";
|
||||
content += `**Reflection Timing:** ${reflectionTiming}\n\n`;
|
||||
}
|
||||
|
||||
// Direct Resolution (if enabled)
|
||||
if (sectionsEnabled.value.directResolution) {
|
||||
content += `## Direct Resolution\n\n`;
|
||||
content += `A *direct resolution* process occurs when individuals communicate their concerns and work together to resolve disputes without filing an informal or formal complaint.\n\n`;
|
||||
|
||||
content += `### Have a Conversation\n\n`;
|
||||
content += `When there is a disagreement, the involved people should first **communicate with each other** about their concerns.\n\n`;
|
||||
|
||||
content += `1. **Choose a time and place** to meet that is private and agreeable to both.\n`;
|
||||
content += `2. **Allow reasonable time** for the conversation.\n`;
|
||||
content += `3. **The point is mutual understanding**, not determining who is right or wrong. This requires patience and willingness to listen without immediately dismissing the other person's perspective.\n`;
|
||||
content += `4. **Express thoughts and feelings directly** without belittling or dismissing. Use "I" statements and active listening techniques.\n`;
|
||||
content += `5. **Communicate your wants and needs** and make offers and requests.\n`;
|
||||
content += `6. **Learn for the future.** Ask questions like, "If what I/you said or did came across that way, what can we do to prevent this from happening in the future?"\n`;
|
||||
|
||||
if (formData.value.documentDirectResolution) {
|
||||
content += `7. **Keep a written record** of the resolution agreed to by both parties.\n\n`;
|
||||
} else {
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
// Communication Channels
|
||||
const selectedChannels = communicationChannels.value.filter(
|
||||
(c) => c.checked
|
||||
);
|
||||
if (selectedChannels.length > 0) {
|
||||
content += `### Escalating Communication Bandwidth\n\n`;
|
||||
content += `Whenever a misunderstanding or conflict arises, **escalate the bandwidth of the channel**:\n\n`;
|
||||
selectedChannels.forEach((channel, index) => {
|
||||
content += `${index + 1}. ${channel.label}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
if (formData.value.requireDirectAttempt) {
|
||||
content += `> **Note:** Direct resolution must be attempted before escalating to assisted resolution, unless safety concerns prevent this.\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Assisted Resolution
|
||||
content += `## Assisted Resolution\n\n`;
|
||||
content += `If talking things out doesn't work, you can ask a responsible contact person for help in writing.\n\n`;
|
||||
|
||||
// Responsible Contact People
|
||||
const selectedReceivers = reportReceivers.value.filter((r) => r.checked);
|
||||
if (selectedReceivers.length > 0) {
|
||||
content += `### Initial Contact Options\n\n`;
|
||||
content += `You can report conflicts to any of the following:\n\n`;
|
||||
selectedReceivers.forEach((receiver) => {
|
||||
content += `- ${receiver.label}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
// Mediator Structure
|
||||
if (formData.value.mediatorType) {
|
||||
content += `### Mediation/Facilitation\n\n`;
|
||||
content += `**Structure:** ${formData.value.mediatorType}\n\n`;
|
||||
|
||||
if (formData.value.supportPeople) {
|
||||
content += `**Support People:** Parties may bring a trusted person for emotional support during mediation sessions.\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Timeline
|
||||
content += `### Response Times\n\n`;
|
||||
if (formData.value.initialResponse) {
|
||||
content += `- **Initial Response:** ${formData.value.initialResponse}\n`;
|
||||
}
|
||||
if (formData.value.resolutionTarget) {
|
||||
content += `- **Target Resolution:** ${formData.value.resolutionTarget}\n\n`;
|
||||
}
|
||||
|
||||
// Formal Complaints
|
||||
content += `## Formal Complaints\n\n`;
|
||||
content += `If assisted resolution efforts do not result in an acceptable outcome within a reasonable timeframe, a *formal complaint* may be filed in writing.\n\n`;
|
||||
|
||||
// Required Elements
|
||||
const selectedElements = formalComplaintElements.value.filter(
|
||||
(e) => e.checked
|
||||
);
|
||||
if (selectedElements.length > 0) {
|
||||
content += `### Written Complaint Requirements\n\n`;
|
||||
content += `The formal complaint must include:\n\n`;
|
||||
selectedElements.forEach((element, index) => {
|
||||
content += `${index + 1}. ${element.label}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
// Formal Process Timeline
|
||||
content += `### Formal Process Timeline\n\n`;
|
||||
if (formData.value.formalAcknowledgmentTime) {
|
||||
content += `- **Acknowledgment:** ${formData.value.formalAcknowledgmentTime}\n`;
|
||||
}
|
||||
if (formData.value.formalReviewTime) {
|
||||
content += `- **Review Completion:** ${formData.value.formalReviewTime}\n\n`;
|
||||
}
|
||||
|
||||
if (formData.value.requireExternalAdvice) {
|
||||
content += `> **External Expertise:** For complex complaints involving multiple parties or organizational leaders, external legal advice will be sought.\n\n`;
|
||||
}
|
||||
|
||||
// Settlement Documentation
|
||||
if (formData.value.requireMinutesOfSettlement) {
|
||||
content += `### Reaching Agreement\n\n`;
|
||||
content += `Any resolution agreed upon must be documented in "Minutes of Settlement" signed by both parties. These agreements will be kept confidential according to our privacy standards.\n\n`;
|
||||
}
|
||||
|
||||
// Consequences and Actions
|
||||
const selectedActions = availableActions.value.filter((a) => a.checked);
|
||||
if (selectedActions.length > 0) {
|
||||
content += `## Possible Outcomes\n\n`;
|
||||
content += `Depending on the situation, resolution may include:\n\n`;
|
||||
selectedActions.forEach((action) => {
|
||||
content += `- ${action.label}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
if (formData.value.appealProcess) {
|
||||
content += `### Appeals Process\n\n`;
|
||||
content += `Parties may request review of decisions through our appeals process.\n\n`;
|
||||
}
|
||||
|
||||
// Documentation and Privacy
|
||||
if (sectionsEnabled.value.documentation) {
|
||||
content += `## Documentation & Privacy\n\n`;
|
||||
if (formData.value.docLevel) {
|
||||
content += `**Documentation Level:** ${formData.value.docLevel}\n\n`;
|
||||
}
|
||||
if (formData.value.confidentiality) {
|
||||
content += `**Confidentiality:** ${formData.value.confidentiality}\n\n`;
|
||||
}
|
||||
if (formData.value.retention) {
|
||||
content += `**Record Retention:** ${formData.value.retention}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// External Resources (if enabled)
|
||||
if (sectionsEnabled.value.externalResources) {
|
||||
content += `## External Resources\n\n`;
|
||||
if (formData.value.includeHumanRights) {
|
||||
content += `Individuals who are not satisfied with the outcome of a harassment or discrimination complaint may file a complaint with the [Canadian Human Rights Commission](https://www.chrc-ccdp.gc.ca/eng) or their provincial human rights tribunal.\n\n`;
|
||||
}
|
||||
|
||||
if (formData.value.additionalResources) {
|
||||
content += `### Additional Resources\n\n`;
|
||||
content += `${formData.value.additionalResources}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation
|
||||
content += `## Policy Management\n\n`;
|
||||
if (formData.value.training) {
|
||||
content += `### Training Requirements\n\n`;
|
||||
content += `${formData.value.training}\n\n`;
|
||||
}
|
||||
|
||||
content += `### Review and Updates\n\n`;
|
||||
if (formData.value.reviewSchedule) {
|
||||
content += `This policy will be reviewed ${formData.value.reviewSchedule.toLowerCase()}.\n\n`;
|
||||
}
|
||||
if (formData.value.amendments) {
|
||||
content += `**Amendment Process:** ${formData.value.amendments}\n\n`;
|
||||
}
|
||||
|
||||
// Acknowledgments
|
||||
if (formData.value.acknowledgments) {
|
||||
content += `### Acknowledgments\n\n`;
|
||||
content += `${formData.value.acknowledgments}\n\n`;
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
// Export data for the ExportOptions component - structured to match ExportOptions expectations
|
||||
const exportData = computed(() => {
|
||||
// Get selected values for arrays
|
||||
const selectedCoreValues = coreValues.value
|
||||
.filter((v) => v.checked)
|
||||
.map((v) => v.label);
|
||||
const selectedConflictTypes = conflictTypes.value
|
||||
.filter((c) => c.checked)
|
||||
.map((c) => c.label);
|
||||
const selectedProcessSteps = processSteps.value
|
||||
.filter((s) => s.checked)
|
||||
.map((s) => s.label);
|
||||
const selectedActions = availableActions.value
|
||||
.filter((a) => a.checked)
|
||||
.map((a) => a.label);
|
||||
const selectedReceivers = reportReceivers.value
|
||||
.filter((r) => r.checked)
|
||||
.map((r) => r.label);
|
||||
const selectedChannels = communicationChannels.value
|
||||
.filter((c) => c.checked)
|
||||
.map((c) => c.label);
|
||||
const selectedComplaintElements = formalComplaintElements.value
|
||||
.filter((e) => e.checked)
|
||||
.map((e) => e.label);
|
||||
const selectedCircumstances = specialCircumstances.value
|
||||
.filter((c) => c.checked)
|
||||
.map((c) => c.label);
|
||||
|
||||
return {
|
||||
section: "conflict-resolution-framework",
|
||||
// Enhanced formData with processed arrays
|
||||
formData: {
|
||||
...formData.value,
|
||||
// Add processed arrays as lists for the formatter
|
||||
coreValuesList: selectedCoreValues,
|
||||
conflictTypesList: selectedConflictTypes,
|
||||
processStepsList: selectedProcessSteps,
|
||||
actionsList: selectedActions,
|
||||
receiversList: selectedReceivers,
|
||||
channelsList: selectedChannels,
|
||||
complaintElementsList: selectedComplaintElements,
|
||||
circumstancesList: selectedCircumstances,
|
||||
},
|
||||
sectionsEnabled: sectionsEnabled.value,
|
||||
reportReceivers: reportReceivers.value,
|
||||
coreValues: coreValues.value,
|
||||
conflictTypes: conflictTypes.value,
|
||||
processSteps: processSteps.value,
|
||||
availableActions: availableActions.value,
|
||||
specialCircumstances: specialCircumstances.value,
|
||||
communicationChannels: communicationChannels.value,
|
||||
formalComplaintElements: formalComplaintElements.value,
|
||||
exportedAt: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue