refactor: enhance ProjectBudgetEstimate component layout, improve budget estimation calculations, and update CSS for better visual consistency and dark mode support
This commit is contained in:
parent
f073f91569
commit
b6e8d3b7ec
6 changed files with 502 additions and 358 deletions
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<section class="py-8 space-y-6">
|
||||
<section class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<h2 class="text-2xl font-bold">Budget Worksheet</h2>
|
||||
|
|
|
|||
|
|
@ -1,84 +1,130 @@
|
|||
<template>
|
||||
<div class="space-y-8">
|
||||
<div class="text-center">
|
||||
<h1 class="text-3xl font-bold mb-4">Project Budget Estimate</h1>
|
||||
<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.
|
||||
<div class="">
|
||||
<h1 class="font-bold text-2xl mb-4">Project Budget Estimate</h1>
|
||||
<p class="text-neutral-600 dark:text-neutral-400 mx-auto mb-4">
|
||||
This tool provides a rough estimate of what it would cost to build your
|
||||
project using the pay policy you've set in the setup.
|
||||
</p>
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 max-w-2xl mx-auto">
|
||||
<div class="flex items-start gap-2">
|
||||
<UIcon name="i-heroicons-information-circle" class="h-5 w-5 text-blue-500 mt-0.5 flex-shrink-0" />
|
||||
<div class="text-sm text-blue-800">
|
||||
<p class="font-medium mb-1">About the calculations:</p>
|
||||
<p>These estimates are based on <strong>sustainable payroll</strong> — what you can actually afford to pay based on your revenue minus overhead costs. This may be different from theoretical maximum wages if revenue is limited.</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<!-- Sustainable payroll toggle hidden - defaulting to theoretical maximum -->
|
||||
<div class="hidden">
|
||||
<span class="text-sm font-medium">Sustainable Payroll</span>
|
||||
<USwitch v-model="useTheoreticalPayroll" size="md" />
|
||||
<span class="text-sm font-medium">Theoretical Maximum</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="membersWithPay.length === 0" class="text-center py-8">
|
||||
<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-neutral-100"
|
||||
>
|
||||
<p class="text-neutral-600 dark:text-neutral-400 mb-4">
|
||||
No team members set up yet.
|
||||
</p>
|
||||
<NuxtLink
|
||||
to="/coop-builder"
|
||||
class="px-4 py-2 border-2 border-black dark:border-white bg-white dark:bg-black text-black dark:text-white font-bold hover:bg-neutral-100 dark:hover:bg-neutral-900">
|
||||
Set up your team in Setup Wizard
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<ProjectBudgetEstimate
|
||||
|
||||
<ProjectBudgetEstimate
|
||||
v-else
|
||||
:members="membersWithPay"
|
||||
:members="membersWithPay"
|
||||
:oncost-rate="coopStore.payrollOncostPct / 100"
|
||||
/>
|
||||
:payroll-mode="useTheoreticalPayroll ? 'theoretical' : 'sustainable'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const coopStore = useCoopBuilderStore()
|
||||
const budgetStore = useBudgetStore()
|
||||
import { allocatePayroll as allocatePayrollImpl } from "~/types/members";
|
||||
|
||||
// Calculate member pay using the same allocation logic as the budget system
|
||||
const coopStore = useCoopBuilderStore();
|
||||
const budgetStore = useBudgetStore();
|
||||
|
||||
// Toggle between sustainable and theoretical payroll modes - defaulting to theoretical maximum
|
||||
const useTheoreticalPayroll = ref(true);
|
||||
|
||||
// Calculate member pay using different logic based on payroll mode
|
||||
const membersWithPay = computed(() => {
|
||||
// Get current month's payroll from budget store (matches budget page)
|
||||
const today = new Date()
|
||||
const currentMonthKey = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}`
|
||||
|
||||
const payrollExpense = budgetStore.budgetWorksheet.expenses.find(item =>
|
||||
item.id === "expense-payroll-base" || item.id === "expense-payroll"
|
||||
)
|
||||
const actualPayrollBudget = payrollExpense?.monthlyValues?.[currentMonthKey] || 0
|
||||
|
||||
// Use the member's desired hours (targetHours if available, otherwise hoursPerMonth)
|
||||
const getHoursForMember = (member: any) => {
|
||||
return member.capacity?.targetHours || member.hoursPerMonth || 0
|
||||
return member.capacity?.targetHours || member.hoursPerMonth || 0;
|
||||
};
|
||||
|
||||
let allocatedMembers;
|
||||
|
||||
if (useTheoreticalPayroll.value) {
|
||||
// Theoretical mode: Calculate true theoretical maximum without revenue constraints
|
||||
const allMembers = coopStore.members.map((m: any) => ({
|
||||
...m,
|
||||
displayName: m.name,
|
||||
monthlyPayPlanned: m.monthlyPayPlanned || 0,
|
||||
minMonthlyNeeds: m.minMonthlyNeeds || 0,
|
||||
hoursPerMonth: m.hoursPerMonth || 0,
|
||||
}));
|
||||
|
||||
const payPolicy = {
|
||||
relationship: coopStore.policy.relationship || ("equal-pay" as const),
|
||||
};
|
||||
|
||||
// Calculate theoretical maximum budget: total hours × hourly wage
|
||||
const totalHours = allMembers.reduce(
|
||||
(sum, m) => sum + (m.hoursPerMonth || 0),
|
||||
0
|
||||
);
|
||||
const hourlyWage = coopStore.equalHourlyWage || 0;
|
||||
const theoreticalMaxBudget = totalHours * hourlyWage;
|
||||
|
||||
allocatedMembers = allocatePayrollImpl(
|
||||
allMembers,
|
||||
payPolicy,
|
||||
theoreticalMaxBudget
|
||||
);
|
||||
} else {
|
||||
// Sustainable mode: Use revenue-constrained allocation (current behavior)
|
||||
const { allocatePayroll } = useCoopBuilder();
|
||||
const sustainableMembers = allocatePayroll();
|
||||
|
||||
const today = new Date();
|
||||
const currentMonthKey = `${today.getFullYear()}-${String(
|
||||
today.getMonth() + 1
|
||||
).padStart(2, "0")}`;
|
||||
|
||||
const payrollExpense = budgetStore.budgetWorksheet.expenses.find(
|
||||
(item) =>
|
||||
item.id === "expense-payroll-base" || item.id === "expense-payroll"
|
||||
);
|
||||
const actualPayrollBudget =
|
||||
payrollExpense?.monthlyValues?.[currentMonthKey] || 0;
|
||||
|
||||
const theoreticalTotal = sustainableMembers.reduce(
|
||||
(sum, m) => sum + (m.monthlyPayPlanned || 0),
|
||||
0
|
||||
);
|
||||
const scaleFactor =
|
||||
theoreticalTotal > 0 ? actualPayrollBudget / theoreticalTotal : 0;
|
||||
|
||||
allocatedMembers = sustainableMembers.map((member) => ({
|
||||
...member,
|
||||
monthlyPayPlanned: (member.monthlyPayPlanned || 0) * scaleFactor,
|
||||
}));
|
||||
}
|
||||
|
||||
// Get theoretical allocation then scale to actual budget
|
||||
const { allocatePayroll } = useCoopBuilder()
|
||||
const theoreticalMembers = allocatePayroll()
|
||||
const theoreticalTotal = theoreticalMembers.reduce((sum, m) => sum + (m.monthlyPayPlanned || 0), 0)
|
||||
const scaleFactor = theoreticalTotal > 0 ? actualPayrollBudget / theoreticalTotal : 0
|
||||
|
||||
const allocatedMembers = theoreticalMembers.map(member => ({
|
||||
...member,
|
||||
monthlyPayPlanned: (member.monthlyPayPlanned || 0) * scaleFactor
|
||||
}))
|
||||
|
||||
return allocatedMembers.map(member => {
|
||||
const hours = getHoursForMember(member)
|
||||
|
||||
return {
|
||||
name: member.name || 'Unnamed',
|
||||
hoursPerMonth: hours,
|
||||
monthlyPay: member.monthlyPayPlanned || 0
|
||||
}
|
||||
}).filter(m => m.hoursPerMonth > 0) // Only include members with hours
|
||||
})
|
||||
|
||||
return allocatedMembers
|
||||
.map((member: any) => {
|
||||
const hours = getHoursForMember(member);
|
||||
|
||||
return {
|
||||
name: member.displayName || "Unnamed",
|
||||
hoursPerMonth: hours,
|
||||
monthlyPay: member.monthlyPayPlanned || 0,
|
||||
};
|
||||
})
|
||||
.filter((m: any) => m.hoursPerMonth > 0); // Only include members with hours
|
||||
});
|
||||
|
||||
// Set page meta
|
||||
definePageMeta({
|
||||
title: 'Project Budget Estimate'
|
||||
})
|
||||
</script>
|
||||
title: "Project Budget Estimate",
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@
|
|||
</h3>
|
||||
<p class="text-neutral-700 dark:text-neutral-200 mb-4">
|
||||
A Miro template to help your team align on shared goals and
|
||||
values through collaborative exercises.
|
||||
values through collaborative exercises. Make sure to do this
|
||||
WITH your full team!
|
||||
</p>
|
||||
<a
|
||||
href="https://miro.com/miroverse/goals-values-exercise/"
|
||||
|
|
@ -92,64 +93,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PDF Downloads Section -->
|
||||
<section>
|
||||
<h2
|
||||
class="text-2xl font-semibold text-neutral-900 dark:text-white mb-4">
|
||||
Wizard PDF Downloads
|
||||
</h2>
|
||||
<p class="text-neutral-700 dark:text-neutral-200 mb-4">
|
||||
Download PDF versions of our wizards for offline use or printing.
|
||||
</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div
|
||||
v-for="pdf in pdfDownloads"
|
||||
:key="pdf.id"
|
||||
class="template-card">
|
||||
<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">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<h3
|
||||
class="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
{{ pdf.name }}
|
||||
</h3>
|
||||
<span
|
||||
class="text-xs text-neutral-600 dark:text-neutral-400 bg-neutral-100 dark:bg-neutral-800 px-2 py-1 rounded">
|
||||
PDF
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
class="text-sm text-neutral-700 dark:text-neutral-200 mb-4">
|
||||
{{ pdf.description }}
|
||||
</p>
|
||||
<button
|
||||
:disabled="!pdf.available"
|
||||
:class="[
|
||||
'inline-flex items-center px-4 py-2 font-medium transition-opacity',
|
||||
pdf.available
|
||||
? 'bg-black dark:bg-white text-white dark:text-black hover:opacity-90'
|
||||
: 'bg-neutral-200 dark:bg-neutral-800 text-neutral-500 dark:text-neutral-500 cursor-not-allowed',
|
||||
]">
|
||||
<svg
|
||||
class="w-4 h-4 mr-2"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
{{ pdf.available ? "Download" : "Coming Soon" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,12 +24,13 @@
|
|||
<!-- Purpose Section -->
|
||||
<div class="section-card">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-neutral-800 mb-4">
|
||||
<h2
|
||||
class="text-2xl font-bold text-neutral-800 dark:text-white font-display mb-4">
|
||||
Charter Purpose
|
||||
</h2>
|
||||
<p class="text-neutral-600 mb-4">
|
||||
<p class="text-neutral-600 dark:text-neutral-400 mb-4">
|
||||
Describe what this charter will guide and why it matters to
|
||||
your group.
|
||||
you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -44,10 +45,11 @@
|
|||
<!-- Unified Principles & Importance Section -->
|
||||
<div class="section-card">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-neutral-800 mb-4">
|
||||
<h2
|
||||
class="text-2xl font-bold text-neutral-800 dark:text-white font-display mb-4">
|
||||
Define Your Principles & Importance
|
||||
</h2>
|
||||
<p class="text-neutral-600 mb-6">
|
||||
<p class="text-neutral-600 dark:text-neutral-400 mb-6">
|
||||
Select principles and set their importance. Zero means
|
||||
excluded, 5 means critical.
|
||||
</p>
|
||||
|
|
@ -67,7 +69,7 @@
|
|||
:class="[
|
||||
'relative transition-all',
|
||||
principleWeights[principle.id] > 0
|
||||
? 'item-selected border-2 border-black dark:border-white bg-white dark:bg-neutral-950'
|
||||
? 'item-selected border-2 border-black dark:border-neutral-400 bg-white dark:bg-neutral-950'
|
||||
: 'border border-black dark:border-white bg-transparent',
|
||||
]">
|
||||
<div class="p-6">
|
||||
|
|
@ -90,7 +92,7 @@
|
|||
? 'text-neutral-700'
|
||||
: 'text-neutral-600'
|
||||
"
|
||||
class="text-sm">
|
||||
class="text-sm dark:text-neutral-200">
|
||||
{{ principle.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -139,7 +141,7 @@
|
|||
<!-- 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 dark:border-neutral-800">
|
||||
<label
|
||||
:class="[
|
||||
'flex items-center gap-3 cursor-pointer item-label-bg px-2 py-1',
|
||||
|
|
@ -152,7 +154,7 @@
|
|||
:checked="nonNegotiables.includes(principle.id)"
|
||||
@change="toggleNonNegotiable(principle.id)"
|
||||
class="w-4 h-4" />
|
||||
<span class="text-sm font-medium text-red-600">
|
||||
<span class="text-sm font-medium text-white">
|
||||
Make this non-negotiable
|
||||
</span>
|
||||
</label>
|
||||
|
|
@ -163,7 +165,7 @@
|
|||
v-if="principleWeights[principle.id] > 0"
|
||||
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">
|
||||
class="text-xs font-bold uppercase text-neutral-300 mb-1">
|
||||
Evaluation Criteria:
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
|
|
@ -180,15 +182,17 @@
|
|||
<div class="section-card">
|
||||
<div>
|
||||
<h2
|
||||
class="text-2xl font-bold text-neutral-800 mb-2"
|
||||
class="text-2xl font-bold text-neutral-800 dark:text-white font-display mb-2"
|
||||
id="constraints-heading">
|
||||
Technical Constraints
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<fieldset class="bg-neutral-50 p-6 rounded-lg">
|
||||
<legend class="font-semibold text-lg">Authentication</legend>
|
||||
<fieldset class="bg-neutral-50 dark:bg-neutral-800 p-6">
|
||||
<legend class="font-semibold text-lg dark:text-neutral-200">
|
||||
Authentication
|
||||
</legend>
|
||||
<div
|
||||
class="flex flex-wrap gap-3 constraint-buttons"
|
||||
role="radiogroup"
|
||||
|
|
@ -209,7 +213,7 @@
|
|||
:class="[
|
||||
'relative px-5 py-2 border-2 transition-all focus:outline-none focus:ring-1 focus:ring-black cursor-pointer',
|
||||
constraints.sso === option.value
|
||||
? 'constraint-selected border-black dark:border-white cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
|
||||
? 'constraint-selected border-black dark:border-white cursor-pointer !bg-black dark:!text-black'
|
||||
: 'border-neutral-300 hover:border-neutral-400 bg-white dark:bg-neutral-950 cursor-pointer',
|
||||
]">
|
||||
{{ option.label }}
|
||||
|
|
@ -218,8 +222,10 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="bg-neutral-50 p-6 rounded-lg">
|
||||
<legend class="font-semibold text-lg">Hosting Model</legend>
|
||||
<fieldset class="bg-neutral-50 p-6 dark:bg-neutral-800">
|
||||
<legend class="font-semibold text-lg dark:text-neutral-200">
|
||||
Hosting Model
|
||||
</legend>
|
||||
<div
|
||||
class="flex flex-wrap gap-3 constraint-buttons"
|
||||
role="radiogroup"
|
||||
|
|
@ -240,7 +246,7 @@
|
|||
:class="[
|
||||
'relative px-5 py-2 border-2 transition-all focus:outline-none focus:ring-1 focus:ring-black cursor-pointer',
|
||||
constraints.hosting === option.value
|
||||
? 'constraint-selected border-black dark:border-white cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
|
||||
? 'constraint-selected border-black dark:border-neutral-400 cursor-pointer '
|
||||
: 'border-neutral-300 hover:border-neutral-400 bg-white dark:bg-neutral-950 cursor-pointer',
|
||||
]">
|
||||
{{ option.label }}
|
||||
|
|
@ -249,11 +255,12 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="bg-neutral-50 p-6 rounded-lg">
|
||||
<legend class="font-semibold text-lg">
|
||||
<fieldset class="bg-neutral-50 p-6 dark:bg-neutral-800">
|
||||
<legend class="font-semibold text-lg dark:text-neutral-200">
|
||||
Required Integrations
|
||||
</legend>
|
||||
<p class="text-sm text-neutral-600 mb-4">
|
||||
<p
|
||||
class="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
|
||||
Select all that apply
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-3 constraint-buttons">
|
||||
|
|
@ -273,7 +280,7 @@
|
|||
: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'
|
||||
? 'constraint-selected border-black dark:border-neutral-400 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 }}
|
||||
|
|
@ -282,7 +289,7 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="bg-neutral-50 p-6 rounded-lg">
|
||||
<fieldset class="bg-neutral-50 p-6 dark:bg-neutral-800">
|
||||
<legend class="font-semibold text-lg">
|
||||
Support Expectations
|
||||
</legend>
|
||||
|
|
@ -306,7 +313,7 @@
|
|||
:class="[
|
||||
'relative px-5 py-2 border-2 transition-all focus:outline-none focus:ring-1 focus:ring-black cursor-pointer',
|
||||
constraints.support === option.value
|
||||
? 'constraint-selected border-black dark:border-white cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
|
||||
? 'constraint-selected border-black dark:border-neutral-400 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 }}
|
||||
|
|
@ -315,8 +322,8 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="bg-neutral-50 p-6 rounded-lg">
|
||||
<legend class="font-semibold text-lg">
|
||||
<fieldset class="bg-neutral-50 p-6 dark:bg-neutral-800">
|
||||
<legend class="font-semibold text-lg dark:text-neutral-200">
|
||||
Migration Timeline
|
||||
</legend>
|
||||
<div
|
||||
|
|
@ -339,7 +346,7 @@
|
|||
:class="[
|
||||
'relative px-5 py-2 border-2 transition-all focus:outline-none focus:ring-1 focus:ring-black cursor-pointer',
|
||||
constraints.timeline === option.value
|
||||
? 'constraint-selected border-black dark:border-white cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
|
||||
? 'constraint-selected border-black dark:border-neutral-400 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 }}
|
||||
|
|
@ -406,10 +413,8 @@
|
|||
<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, so
|
||||
that we can choose tools that don't contradict our values.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -672,7 +677,7 @@ definePageMeta({
|
|||
|
||||
// State
|
||||
const charterPurpose = ref(
|
||||
"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, so that we can choose tools that don't contradict our values."
|
||||
);
|
||||
const principleWeights = ref({});
|
||||
const nonNegotiables = ref([]);
|
||||
|
|
@ -690,7 +695,7 @@ const constraints = ref({
|
|||
const principles = [
|
||||
{
|
||||
id: "privacy",
|
||||
name: "Privacy & Data Control",
|
||||
name: "Privacy and data control",
|
||||
description: "Data minimization, encryption, sovereignty, and user consent",
|
||||
rubricDescription:
|
||||
"Data collection practices, encryption standards, jurisdiction control",
|
||||
|
|
@ -698,14 +703,14 @@ const principles = [
|
|||
},
|
||||
{
|
||||
id: "accessibility",
|
||||
name: "Universal Access",
|
||||
name: "Universal access",
|
||||
description: "WCAG compliance, screen readers, keyboard navigation",
|
||||
rubricDescription: "WCAG 2.2 AA, keyboard nav, screen reader support",
|
||||
defaultWeight: 5,
|
||||
},
|
||||
{
|
||||
id: "portability",
|
||||
name: "Data Freedom",
|
||||
name: "Data freedom",
|
||||
description: "Easy export, no vendor lock-in, migration-friendly",
|
||||
rubricDescription:
|
||||
"Export capabilities, proprietary formats, switching costs",
|
||||
|
|
@ -713,7 +718,7 @@ const principles = [
|
|||
},
|
||||
{
|
||||
id: "opensource",
|
||||
name: "Open Source & Community",
|
||||
name: "Open source and community",
|
||||
description:
|
||||
"FOSS preference, transparent development, community governance",
|
||||
rubricDescription: "License type, community involvement, code transparency",
|
||||
|
|
@ -721,7 +726,7 @@ const principles = [
|
|||
},
|
||||
{
|
||||
id: "sustainability",
|
||||
name: "Sustainable Operations",
|
||||
name: "Sustainable operations",
|
||||
description: "Predictable costs, green hosting, efficient resource use",
|
||||
rubricDescription:
|
||||
"Total cost of ownership, carbon footprint, resource efficiency",
|
||||
|
|
@ -729,14 +734,14 @@ const principles = [
|
|||
},
|
||||
{
|
||||
id: "localization",
|
||||
name: "Local Support",
|
||||
name: "Local support",
|
||||
description: "Multi-language, timezone aware, cultural sensitivity",
|
||||
rubricDescription: "Language options, cultural awareness, regional support",
|
||||
defaultWeight: 2,
|
||||
},
|
||||
{
|
||||
id: "usability",
|
||||
name: "User Experience",
|
||||
name: "User experience",
|
||||
description:
|
||||
"Intuitive interface, minimal learning curve, daily efficiency",
|
||||
rubricDescription:
|
||||
|
|
@ -746,30 +751,30 @@ const principles = [
|
|||
];
|
||||
|
||||
const authOptions = [
|
||||
{ value: "required", label: "SSO Required" },
|
||||
{ value: "preferred", label: "SSO Preferred" },
|
||||
{ value: "optional", label: "SSO Optional" },
|
||||
{ value: "required", label: "SSO required" },
|
||||
{ value: "preferred", label: "SSO preferred" },
|
||||
{ value: "optional", label: "SSO optional" },
|
||||
];
|
||||
|
||||
const hostingOptions = [
|
||||
{ value: "self", label: "Self-Hosted Only" },
|
||||
{ value: "self", label: "Self-hosted only" },
|
||||
{ value: "either", label: "Either" },
|
||||
{ value: "managed", label: "Managed Only" },
|
||||
{ value: "managed", label: "Managed only" },
|
||||
];
|
||||
|
||||
const integrationOptions = ["Slack", "OIDC/OAuth", "Webhooks", "REST API"];
|
||||
|
||||
const supportOptions = [
|
||||
{ value: "community", label: "Community Only OK" },
|
||||
{ value: "business", label: "Business Hours" },
|
||||
{ value: "24-7", label: "24/7 Required" },
|
||||
{ value: "community", label: "Community only OK" },
|
||||
{ value: "business", label: "Business hours" },
|
||||
{ value: "24-7", label: "24/7 required" },
|
||||
];
|
||||
|
||||
const timelineOptions = [
|
||||
{ value: "immediate", label: "This Month" },
|
||||
{ value: "quarter", label: "This Quarter" },
|
||||
{ value: "year", label: "This Year" },
|
||||
{ value: "exploring", label: "Just Exploring" },
|
||||
{ value: "immediate", label: "This month" },
|
||||
{ value: "quarter", label: "This quarter" },
|
||||
{ value: "year", label: "This year" },
|
||||
{ value: "exploring", label: "Just exploring" },
|
||||
];
|
||||
|
||||
// Computed
|
||||
|
|
@ -853,7 +858,7 @@ const toggleIntegration = (integration) => {
|
|||
const resetForm = () => {
|
||||
if (confirm("Are you sure you want to clear all form data and start over?")) {
|
||||
charterPurpose.value =
|
||||
"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, so that we can choose tools that don't contradict our values.";
|
||||
// Reset all principle weights to 0
|
||||
principles.forEach((p) => {
|
||||
principleWeights.value[p.id] = 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue