refactor: update membership agreement and conflict resolution framework templates to utilize centralized cooperative information, enhance decision framework handling, and improve overall layout for better user experience
This commit is contained in:
parent
f1889b3a70
commit
7b4fb6c2fd
5 changed files with 510 additions and 703 deletions
|
|
@ -424,7 +424,8 @@ const formatObjectAsMarkdown = (obj: any): string => {
|
||||||
// Membership Agreement formatting - Complete document with all static and dynamic content
|
// Membership Agreement formatting - Complete document with all static and dynamic content
|
||||||
const formatMembershipAgreementAsText = (data: any): string => {
|
const formatMembershipAgreementAsText = (data: any): string => {
|
||||||
const formData = data.formData || {};
|
const formData = data.formData || {};
|
||||||
let content = `MEMBERSHIP AGREEMENT\n====================\n\n`;
|
const cooperativeName = data.cooperativeName || formData.cooperativeName || "the cooperative";
|
||||||
|
let content = "";
|
||||||
|
|
||||||
// Section 1: Who We Are
|
// Section 1: Who We Are
|
||||||
content += `1. WHO WE ARE\n-------------\n\n`;
|
content += `1. WHO WE ARE\n-------------\n\n`;
|
||||||
|
|
@ -459,18 +460,25 @@ const formatMembershipAgreementAsText = (data: any): string => {
|
||||||
content += `New members join through a consent process, which means existing members must agree that adding this person won't harm the cooperative.\n\n`;
|
content += `New members join through a consent process, which means existing members must agree that adding this person won't harm the cooperative.\n\n`;
|
||||||
content += `1. Trial period of ${formData.trialPeriodMonths || 3} months working together\n`;
|
content += `1. Trial period of ${formData.trialPeriodMonths || 3} months working together\n`;
|
||||||
content += `2. Values alignment conversation\n`;
|
content += `2. Values alignment conversation\n`;
|
||||||
content += `3. Consent decision by current members\n`;
|
content += `3. Optional - Equal buy-in contribution of $${formData.buyInAmount || "[amount]"} (can be paid over time or waived based on need)\n\n`;
|
||||||
content += `4. Optional - Equal buy-in contribution of $${formData.buyInAmount || "[amount]"} (can be paid over time or waived based on need)\n\n`;
|
|
||||||
|
|
||||||
content += `Leaving the Cooperative:\n`;
|
content += `Leaving the Cooperative:\n`;
|
||||||
content += `Members can leave anytime with ${formData.noticeDays || 30} days notice. The cooperative will:\n\n`;
|
content += `Members can leave anytime with ${formData.noticeDays || 30} days notice. ${cooperativeName} will:\n\n`;
|
||||||
content += `• Pay out their share of any surplus within ${formData.surplusPayoutDays || 30} days\n`;
|
content += `• Pay out their share of any surplus within ${formData.surplusPayoutDays || 30} days\n`;
|
||||||
content += `• Return their buy-in contribution within ${formData.buyInReturnDays || 90} days\n`;
|
content += `• Return their buy-in contribution within ${formData.buyInReturnDays || 90} days\n`;
|
||||||
content += `• Maintain respectful ongoing relationships when possible\n\n`;
|
content += `• Maintain respectful ongoing relationships when possible\n\n`;
|
||||||
|
|
||||||
// Section 3: How We Make Decisions
|
// Section 3: How We Make Decisions
|
||||||
content += `3. HOW WE MAKE DECISIONS\n------------------------\n\n`;
|
content += `3. HOW WE MAKE DECISIONS\n------------------------\n\n`;
|
||||||
content += `Consent-Based Decisions:\nWe 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.\n\n`;
|
|
||||||
|
content += `Primary Decision Framework: ${data.decisionFrameworkName || "Consent-Based - No one objects strongly enough to block"}\n\n`;
|
||||||
|
|
||||||
|
const frameworkDetails = data.decisionFrameworkDetails || {};
|
||||||
|
if (frameworkDetails.practicalDescription) {
|
||||||
|
content += `${frameworkDetails.practicalDescription}\n\n`;
|
||||||
|
} else {
|
||||||
|
content += `We use consent, not consensus. This means we move forward when no one has a principled objection that would harm our organization. An objection must explain how the proposal would contradict our values or threaten our sustainability.\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
content += `Day-to-Day Decisions:\nDecisions under $${formData.dayToDayLimit || 100} can be made by any member. Just tell others what you did at the next meeting.\n\n`;
|
content += `Day-to-Day Decisions:\nDecisions under $${formData.dayToDayLimit || 100} can be made by any member. Just tell others what you did at the next meeting.\n\n`;
|
||||||
|
|
||||||
|
|
@ -481,7 +489,7 @@ const formatMembershipAgreementAsText = (data: any): string => {
|
||||||
content += `• Changing this agreement\n`;
|
content += `• Changing this agreement\n`;
|
||||||
content += `• Taking on debt over $${formData.majorDebtThreshold || 5000}\n`;
|
content += `• Taking on debt over $${formData.majorDebtThreshold || 5000}\n`;
|
||||||
content += `• Fundamental changes to our purpose or structure\n`;
|
content += `• Fundamental changes to our purpose or structure\n`;
|
||||||
content += `• Dissolution of the cooperative\n\n`;
|
content += `• Dissolution of ${cooperativeName}\n\n`;
|
||||||
|
|
||||||
content += `Meeting Structure:\n`;
|
content += `Meeting Structure:\n`;
|
||||||
content += `• Regular meetings happen ${formData.meetingFrequency || "weekly"}\n`;
|
content += `• Regular meetings happen ${formData.meetingFrequency || "weekly"}\n`;
|
||||||
|
|
@ -491,12 +499,12 @@ const formatMembershipAgreementAsText = (data: any): string => {
|
||||||
|
|
||||||
// Section 4: Money and Labour
|
// Section 4: Money and Labour
|
||||||
content += `4. MONEY AND LABOUR\n-------------------\n\n`;
|
content += `4. MONEY AND LABOUR\n-------------------\n\n`;
|
||||||
content += `Equal Ownership:\nEach member owns an equal share of the cooperative, regardless of hours worked or tenure.\n\n`;
|
content += `Equal Ownership:\nEach member owns an equal share of ${cooperativeName}, regardless of hours worked or tenure.\n\n`;
|
||||||
|
|
||||||
content += `Paying Ourselves:\n`;
|
content += `Paying Ourselves:\n`;
|
||||||
const payPolicy = formData.payPolicy || "equal-pay";
|
const payPolicy = formData.payPolicy || "equal-pay";
|
||||||
const payPolicyName = payPolicy.replace('-', ' ').replace(/\b\w/g, (l: string) => l.toUpperCase());
|
const payPolicyName = payPolicy.replace('-', ' ').replace(/\b\w/g, (l: string) => l.toUpperCase());
|
||||||
content += `Pay Policy: ${payPolicyName}\n\n`;
|
content += `Pay Policy: ${data.payPolicyName || payPolicyName}\n\n`;
|
||||||
|
|
||||||
if (payPolicy === 'equal-pay') {
|
if (payPolicy === 'equal-pay') {
|
||||||
content += `All members receive equal compensation regardless of role or hours worked.\n`;
|
content += `All members receive equal compensation regardless of role or hours worked.\n`;
|
||||||
|
|
@ -516,7 +524,7 @@ const formatMembershipAgreementAsText = (data: any): string => {
|
||||||
}
|
}
|
||||||
|
|
||||||
content += `\nPayment Schedule:\n`;
|
content += `\nPayment Schedule:\n`;
|
||||||
content += `• Paid on the ${formData.paymentDay || 15} of each month\n`;
|
content += `• Paid on the ${data.paymentDayLabel || formData.paymentDay || 15} of each month\n`;
|
||||||
content += `• Surplus (profit) distributed equally every ${formData.surplusFrequency || "quarter"}\n\n`;
|
content += `• Surplus (profit) distributed equally every ${formData.surplusFrequency || "quarter"}\n\n`;
|
||||||
|
|
||||||
content += `Work Expectations:\n`;
|
content += `Work Expectations:\n`;
|
||||||
|
|
@ -556,11 +564,13 @@ const formatMembershipAgreementAsText = (data: any): string => {
|
||||||
|
|
||||||
// Section 7: Changing This Agreement
|
// Section 7: Changing This Agreement
|
||||||
content += `7. CHANGING THIS AGREEMENT\n--------------------------\n\n`;
|
content += `7. CHANGING THIS AGREEMENT\n--------------------------\n\n`;
|
||||||
content += `This is a living document. We review it every ${formData.reviewFrequency || "year"} and update it through our consent process. Small clarifications can happen anytime; structural changes need full member consent.\n\n`;
|
const frameworkLabel = data.decisionFrameworkLabel || "consent-based decision";
|
||||||
|
const structuralReq = data.structuralChangeRequirement || "full member consent";
|
||||||
|
content += `This is a living document. We review it every ${formData.reviewFrequency || "year"} and update it through our ${frameworkLabel} process. Small clarifications can happen anytime; structural changes need ${structuralReq}.\n\n`;
|
||||||
|
|
||||||
// Section 8: If We Need to Close
|
// Section 8: If We Need to Close
|
||||||
content += `8. IF WE NEED TO CLOSE\n----------------------\n\n`;
|
content += `8. IF WE NEED TO CLOSE\n----------------------\n\n`;
|
||||||
content += `If the cooperative dissolves:\n`;
|
content += `If ${cooperativeName} dissolves:\n`;
|
||||||
content += `1. Pay all debts and obligations\n`;
|
content += `1. Pay all debts and obligations\n`;
|
||||||
content += `2. Return member contributions\n`;
|
content += `2. Return member contributions\n`;
|
||||||
content += `3. Distribute remaining assets equally among members\n`;
|
content += `3. Distribute remaining assets equally among members\n`;
|
||||||
|
|
@ -569,19 +579,26 @@ const formatMembershipAgreementAsText = (data: any): string => {
|
||||||
}
|
}
|
||||||
content += `\n`;
|
content += `\n`;
|
||||||
|
|
||||||
// Section 9: Legal Bits
|
// Section 9: Legal Registration
|
||||||
content += `9. LEGAL BITS\n-------------\n\n`;
|
content += `9. LEGAL REGISTRATION\n---------------------\n\n`;
|
||||||
content += `Legal Structure: ${formData.legalStructure || "[Cooperative corporation, LLC, partnership, etc.]"}\n`;
|
|
||||||
content += `Registered in: ${formData.registeredLocation || "[State/Province]"}\n`;
|
if (formData.isLegallyRegistered) {
|
||||||
content += `Fiscal Year-End: ${formData.fiscalYearEndMonth || "December"} ${formData.fiscalYearEndDay || 31}\n\n`;
|
content += `Legal Structure: ${formData.legalStructure || "[Cooperative corporation, LLC, partnership, etc.]"}\n`;
|
||||||
content += `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.\n\n`;
|
content += `Registered in: ${formData.registeredLocation || "[State/Province]"}\n`;
|
||||||
|
content += `Fiscal Year-End: ${formData.fiscalYearEndMonth || "December"} ${formData.fiscalYearEndDay || 31}\n\n`;
|
||||||
|
content += `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.\n\n`;
|
||||||
|
} else {
|
||||||
|
const thisCooperative = cooperativeName === "the cooperative" ? "This cooperative" : cooperativeName;
|
||||||
|
content += `${thisCooperative} operates as an informal collective. If we decide to register legally in the future, we'll update this section with our legal structure details.\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatMembershipAgreementAsMarkdown = (data: any): string => {
|
const formatMembershipAgreementAsMarkdown = (data: any): string => {
|
||||||
const formData = data.formData || {};
|
const formData = data.formData || {};
|
||||||
let content = `# Membership Agreement\n\n`;
|
const cooperativeName = data.cooperativeName || formData.cooperativeName || "the cooperative";
|
||||||
|
let content = "";
|
||||||
|
|
||||||
// Section 1: Who We Are
|
// Section 1: Who We Are
|
||||||
content += `## 1. Who We Are\n\n`;
|
content += `## 1. Who We Are\n\n`;
|
||||||
|
|
@ -616,19 +633,26 @@ const formatMembershipAgreementAsMarkdown = (data: any): string => {
|
||||||
content += `New members join through a consent process, which means existing members must agree that adding this person won't harm the cooperative.\n\n`;
|
content += `New members join through a consent process, which means existing members must agree that adding this person won't harm the cooperative.\n\n`;
|
||||||
content += `1. Trial period of **${formData.trialPeriodMonths || 3} months** working together\n`;
|
content += `1. Trial period of **${formData.trialPeriodMonths || 3} months** working together\n`;
|
||||||
content += `2. Values alignment conversation\n`;
|
content += `2. Values alignment conversation\n`;
|
||||||
content += `3. Consent decision by current members\n`;
|
content += `3. Optional - Equal buy-in contribution of **$${formData.buyInAmount || "[amount]"}** (can be paid over time or waived based on need)\n\n`;
|
||||||
content += `4. Optional - Equal buy-in contribution of **$${formData.buyInAmount || "[amount]"}** (can be paid over time or waived based on need)\n\n`;
|
|
||||||
|
|
||||||
content += `### Leaving the Cooperative\n\n`;
|
content += `### Leaving the Cooperative\n\n`;
|
||||||
content += `Members can leave anytime with **${formData.noticeDays || 30} days** notice. The cooperative will:\n\n`;
|
content += `Members can leave anytime with **${formData.noticeDays || 30} days** notice. ${cooperativeName} will:\n\n`;
|
||||||
content += `- Pay out their share of any surplus within **${formData.surplusPayoutDays || 30} days**\n`;
|
content += `- Pay out their share of any surplus within **${formData.surplusPayoutDays || 30} days**\n`;
|
||||||
content += `- Return their buy-in contribution within **${formData.buyInReturnDays || 90} days**\n`;
|
content += `- Return their buy-in contribution within **${formData.buyInReturnDays || 90} days**\n`;
|
||||||
content += `- Maintain respectful ongoing relationships when possible\n\n`;
|
content += `- Maintain respectful ongoing relationships when possible\n\n`;
|
||||||
|
|
||||||
// Section 3: How We Make Decisions
|
// Section 3: How We Make Decisions
|
||||||
content += `## 3. How We Make Decisions\n\n`;
|
content += `## 3. How We Make Decisions\n\n`;
|
||||||
content += `### Consent-Based Decisions\n\n`;
|
|
||||||
content += `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.\n\n`;
|
content += `### Primary Decision Framework\n\n`;
|
||||||
|
content += `**${data.decisionFrameworkName || "Consent-Based - No one objects strongly enough to block"}**\n\n`;
|
||||||
|
|
||||||
|
const frameworkDetails = data.decisionFrameworkDetails || {};
|
||||||
|
if (frameworkDetails.practicalDescription) {
|
||||||
|
content += `${frameworkDetails.practicalDescription}\n\n`;
|
||||||
|
} else {
|
||||||
|
content += `We use consent, not consensus. This means we move forward when no one has a principled objection that would harm our organization. An objection must explain how the proposal would contradict our values or threaten our sustainability.\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
content += `### Day-to-Day Decisions\n\n`;
|
content += `### Day-to-Day Decisions\n\n`;
|
||||||
content += `Decisions under **$${formData.dayToDayLimit || 100}** can be made by any member. Just tell others what you did at the next meeting.\n\n`;
|
content += `Decisions under **$${formData.dayToDayLimit || 100}** can be made by any member. Just tell others what you did at the next meeting.\n\n`;
|
||||||
|
|
@ -642,7 +666,7 @@ const formatMembershipAgreementAsMarkdown = (data: any): string => {
|
||||||
content += `- Changing this agreement\n`;
|
content += `- Changing this agreement\n`;
|
||||||
content += `- Taking on debt over **$${formData.majorDebtThreshold || 5000}**\n`;
|
content += `- Taking on debt over **$${formData.majorDebtThreshold || 5000}**\n`;
|
||||||
content += `- Fundamental changes to our purpose or structure\n`;
|
content += `- Fundamental changes to our purpose or structure\n`;
|
||||||
content += `- Dissolution of the cooperative\n\n`;
|
content += `- Dissolution of ${cooperativeName}\n\n`;
|
||||||
|
|
||||||
content += `### Meeting Structure\n\n`;
|
content += `### Meeting Structure\n\n`;
|
||||||
content += `- Regular meetings happen **${formData.meetingFrequency || "weekly"}**\n`;
|
content += `- Regular meetings happen **${formData.meetingFrequency || "weekly"}**\n`;
|
||||||
|
|
@ -653,12 +677,12 @@ const formatMembershipAgreementAsMarkdown = (data: any): string => {
|
||||||
// Section 4: Money and Labour
|
// Section 4: Money and Labour
|
||||||
content += `## 4. Money and Labour\n\n`;
|
content += `## 4. Money and Labour\n\n`;
|
||||||
content += `### Equal Ownership\n\n`;
|
content += `### Equal Ownership\n\n`;
|
||||||
content += `Each member owns an equal share of the cooperative, regardless of hours worked or tenure.\n\n`;
|
content += `Each member owns an equal share of ${cooperativeName}, regardless of hours worked or tenure.\n\n`;
|
||||||
|
|
||||||
content += `### Paying Ourselves\n\n`;
|
content += `### Paying Ourselves\n\n`;
|
||||||
const payPolicy = formData.payPolicy || "equal-pay";
|
const payPolicy = formData.payPolicy || "equal-pay";
|
||||||
const payPolicyName = payPolicy.replace('-', ' ').replace(/\b\w/g, (l: string) => l.toUpperCase());
|
const payPolicyName = payPolicy.replace('-', ' ').replace(/\b\w/g, (l: string) => l.toUpperCase());
|
||||||
content += `**Pay Policy:** ${payPolicyName}\n\n`;
|
content += `**Pay Policy:** ${data.payPolicyName || payPolicyName}\n\n`;
|
||||||
|
|
||||||
if (payPolicy === 'equal-pay') {
|
if (payPolicy === 'equal-pay') {
|
||||||
content += `All members receive equal compensation regardless of role or hours worked.\n\n`;
|
content += `All members receive equal compensation regardless of role or hours worked.\n\n`;
|
||||||
|
|
@ -678,7 +702,7 @@ const formatMembershipAgreementAsMarkdown = (data: any): string => {
|
||||||
}
|
}
|
||||||
|
|
||||||
content += `\n**Payment Schedule:**\n\n`;
|
content += `\n**Payment Schedule:**\n\n`;
|
||||||
content += `- Paid on the **${formData.paymentDay || 15}** of each month\n`;
|
content += `- Paid on the **${data.paymentDayLabel || formData.paymentDay || 15}** of each month\n`;
|
||||||
content += `- Surplus (profit) distributed equally every **${formData.surplusFrequency || "quarter"}**\n\n`;
|
content += `- Surplus (profit) distributed equally every **${formData.surplusFrequency || "quarter"}**\n\n`;
|
||||||
|
|
||||||
content += `### Work Expectations\n\n`;
|
content += `### Work Expectations\n\n`;
|
||||||
|
|
@ -718,11 +742,13 @@ const formatMembershipAgreementAsMarkdown = (data: any): string => {
|
||||||
|
|
||||||
// Section 7: Changing This Agreement
|
// Section 7: Changing This Agreement
|
||||||
content += `## 7. Changing This Agreement\n\n`;
|
content += `## 7. Changing This Agreement\n\n`;
|
||||||
content += `This is a living document. We review it every **${formData.reviewFrequency || "year"}** and update it through our consent process. Small clarifications can happen anytime; structural changes need full member consent.\n\n`;
|
const frameworkLabel = data.decisionFrameworkLabel || "consent-based decision";
|
||||||
|
const structuralReq = data.structuralChangeRequirement || "full member consent";
|
||||||
|
content += `This is a living document. We review it every **${formData.reviewFrequency || "year"}** and update it through our ${frameworkLabel} process. Small clarifications can happen anytime; structural changes need ${structuralReq}.\n\n`;
|
||||||
|
|
||||||
// Section 8: If We Need to Close
|
// Section 8: If We Need to Close
|
||||||
content += `## 8. If We Need to Close\n\n`;
|
content += `## 8. If We Need to Close\n\n`;
|
||||||
content += `If the cooperative dissolves:\n\n`;
|
content += `If ${cooperativeName} dissolves:\n\n`;
|
||||||
content += `1. Pay all debts and obligations\n`;
|
content += `1. Pay all debts and obligations\n`;
|
||||||
content += `2. Return member contributions\n`;
|
content += `2. Return member contributions\n`;
|
||||||
content += `3. Distribute remaining assets equally among members\n`;
|
content += `3. Distribute remaining assets equally among members\n`;
|
||||||
|
|
@ -731,12 +757,18 @@ const formatMembershipAgreementAsMarkdown = (data: any): string => {
|
||||||
}
|
}
|
||||||
content += `\n`;
|
content += `\n`;
|
||||||
|
|
||||||
// Section 9: Legal Bits
|
// Section 9: Legal Registration
|
||||||
content += `## 9. Legal Bits\n\n`;
|
content += `## 9. Legal Registration\n\n`;
|
||||||
content += `- **Legal Structure:** ${formData.legalStructure || "[Cooperative corporation, LLC, partnership, etc.]"}\n`;
|
|
||||||
content += `- **Registered in:** ${formData.registeredLocation || "[State/Province]"}\n`;
|
if (formData.isLegallyRegistered) {
|
||||||
content += `- **Fiscal Year-End:** ${formData.fiscalYearEndMonth || "December"} ${formData.fiscalYearEndDay || 31}\n\n`;
|
content += `- **Legal Structure:** ${formData.legalStructure || "[Cooperative corporation, LLC, partnership, etc.]"}\n`;
|
||||||
content += `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.\n\n`;
|
content += `- **Registered in:** ${formData.registeredLocation || "[State/Province]"}\n`;
|
||||||
|
content += `- **Fiscal Year-End:** ${formData.fiscalYearEndMonth || "December"} ${formData.fiscalYearEndDay || 31}\n\n`;
|
||||||
|
content += `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.\n\n`;
|
||||||
|
} else {
|
||||||
|
const thisCooperative = cooperativeName === "the cooperative" ? "This cooperative" : cooperativeName;
|
||||||
|
content += `${thisCooperative} operates as an informal collective. If we decide to register legally in the future, we'll update this section with our legal structure details.\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
81
composables/useCoopInfo.ts
Normal file
81
composables/useCoopInfo.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { ref, watch, readonly } from 'vue'
|
||||||
|
|
||||||
|
export interface CoopInfo {
|
||||||
|
cooperativeName: string
|
||||||
|
dateEstablished: string
|
||||||
|
purpose: string
|
||||||
|
coreValues: string
|
||||||
|
legalStructure: string
|
||||||
|
registeredLocation: string
|
||||||
|
isLegallyRegistered: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'coop-info'
|
||||||
|
|
||||||
|
// Global reactive state
|
||||||
|
const coopInfo = ref<CoopInfo>({
|
||||||
|
cooperativeName: '',
|
||||||
|
dateEstablished: '',
|
||||||
|
purpose: '',
|
||||||
|
coreValues: '',
|
||||||
|
legalStructure: '',
|
||||||
|
registeredLocation: '',
|
||||||
|
isLegallyRegistered: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Flag to prevent loading during initial save
|
||||||
|
let isInitialized = false
|
||||||
|
|
||||||
|
export const useCoopInfo = () => {
|
||||||
|
|
||||||
|
// Load data from localStorage on first use
|
||||||
|
if (!isInitialized && process.client) {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(saved)
|
||||||
|
coopInfo.value = { ...coopInfo.value, ...parsedData }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading coop info:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isInitialized = true
|
||||||
|
|
||||||
|
// Set up watcher to save changes
|
||||||
|
watch(
|
||||||
|
coopInfo,
|
||||||
|
(newData) => {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(newData))
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to update specific fields
|
||||||
|
const updateCoopInfo = (updates: Partial<CoopInfo>) => {
|
||||||
|
coopInfo.value = { ...coopInfo.value, ...updates }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get display name (with fallback)
|
||||||
|
const getDisplayName = () => {
|
||||||
|
return coopInfo.value.cooperativeName || 'Worker Cooperative'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get organization name for different contexts
|
||||||
|
const getOrgName = () => {
|
||||||
|
return coopInfo.value.cooperativeName || 'Organization'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if basic info is complete
|
||||||
|
const isBasicInfoComplete = () => {
|
||||||
|
return !!(coopInfo.value.cooperativeName && coopInfo.value.cooperativeName.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
coopInfo: readonly(coopInfo),
|
||||||
|
updateCoopInfo,
|
||||||
|
getDisplayName,
|
||||||
|
getOrgName,
|
||||||
|
isBasicInfoComplete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,596 +0,0 @@
|
||||||
<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>
|
|
||||||
</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";
|
|
||||||
// REMOVED: All sample data imports to prevent demo data
|
|
||||||
|
|
||||||
// Store integration
|
|
||||||
const planStore = usePlanStore();
|
|
||||||
|
|
||||||
// Initialize with empty data
|
|
||||||
const members = ref<Member[]>([]);
|
|
||||||
|
|
||||||
const availableSkills = ref<SkillTag[]>([]);
|
|
||||||
|
|
||||||
const availableProblems = ref<ProblemTag[]>([]);
|
|
||||||
|
|
||||||
// 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');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 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>
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-8">
|
||||||
<h1
|
<h1
|
||||||
class="text-3xl md:text-5xl font-bold uppercase m-0 py-4 border-t-2 border-b-2 text-black dark:text-white border-black dark:border-white"
|
class="text-3xl md:text-5xl font-bold uppercase m-0 py-4 border-t-2 border-b-2 text-black dark:text-white border-black dark:border-white"
|
||||||
:data-org-name="formData.orgName || 'Organization'">
|
:data-org-name="getOrgName()">
|
||||||
CONFLICT RESOLUTION
|
CONFLICT RESOLUTION
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -895,6 +895,9 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, computed, onMounted } from "vue";
|
import { ref, watch, computed, onMounted } from "vue";
|
||||||
|
|
||||||
|
// Import centralized coop info
|
||||||
|
const { coopInfo, updateCoopInfo, getOrgName } = useCoopInfo();
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: false,
|
layout: false,
|
||||||
});
|
});
|
||||||
|
|
@ -1429,6 +1432,27 @@ function debounce(func, wait) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync with centralized coop info
|
||||||
|
watch(
|
||||||
|
() => coopInfo.value,
|
||||||
|
(newCoopInfo) => {
|
||||||
|
if (newCoopInfo.cooperativeName) {
|
||||||
|
formData.value.orgName = newCoopInfo.cooperativeName;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update centralized store when org name changes
|
||||||
|
watch(
|
||||||
|
() => formData.value.orgName,
|
||||||
|
(newOrgName) => {
|
||||||
|
if (newOrgName && newOrgName !== coopInfo.value.cooperativeName) {
|
||||||
|
updateCoopInfo({ cooperativeName: newOrgName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Watch for changes and auto-save
|
// Watch for changes and auto-save
|
||||||
watch(
|
watch(
|
||||||
[
|
[
|
||||||
|
|
@ -1450,7 +1474,7 @@ watch(
|
||||||
// Export data for the ExportOptions component
|
// Export data for the ExportOptions component
|
||||||
const exportData = computed(() => ({
|
const exportData = computed(() => ({
|
||||||
formData: formData.value,
|
formData: formData.value,
|
||||||
orgName: formData.value.orgName || "Organization",
|
orgName: getOrgName(),
|
||||||
orgType: formData.value.orgType,
|
orgType: formData.value.orgType,
|
||||||
memberCount: formData.value.memberCount,
|
memberCount: formData.value.memberCount,
|
||||||
sectionsEnabled: sectionsEnabled.value,
|
sectionsEnabled: sectionsEnabled.value,
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,7 @@
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-8">
|
||||||
<h1
|
<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"
|
||||||
:data-coop-name="
|
:data-coop-name="getDisplayName()">
|
||||||
formData.cooperativeName || 'Worker Cooperative'
|
|
||||||
">
|
|
||||||
MEMBERSHIP AGREEMENT
|
MEMBERSHIP AGREEMENT
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -172,7 +170,7 @@
|
||||||
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
||||||
Any person who:
|
Any person who:
|
||||||
</p>
|
</p>
|
||||||
<UFormField label="Member Requirements" class="form-group-large">
|
<UFormField class="form-group-large">
|
||||||
<UTextarea
|
<UTextarea
|
||||||
v-model="formData.memberRequirements"
|
v-model="formData.memberRequirements"
|
||||||
:rows="4"
|
:rows="4"
|
||||||
|
|
@ -193,7 +191,7 @@
|
||||||
<p class="content-paragraph">
|
<p class="content-paragraph">
|
||||||
New members join through a consent process, which means
|
New members join through a consent process, which means
|
||||||
existing members must agree that adding this person won't harm
|
existing members must agree that adding this person won't harm
|
||||||
the cooperative.
|
{{ getDisplayName().toLowerCase() }}.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ol class="content-list numbered my-2 pl-6 list-decimal">
|
<ol class="content-list numbered my-2 pl-6 list-decimal">
|
||||||
|
|
@ -203,18 +201,17 @@
|
||||||
v-model="formData.trialPeriodMonths"
|
v-model="formData.trialPeriodMonths"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="3"
|
placeholder="3"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />
|
@change="autoSave" />
|
||||||
months working together
|
months working together
|
||||||
</li>
|
</li>
|
||||||
<li>Values alignment conversation</li>
|
<li>Values alignment conversation</li>
|
||||||
<li>Consent decision by current members</li>
|
|
||||||
<li class="flex items-baseline gap-2 flex-wrap">
|
<li class="flex items-baseline gap-2 flex-wrap">
|
||||||
Optional - Equal buy-in contribution of $<UInput
|
Optional - Equal buy-in contribution of $<UInput
|
||||||
v-model="formData.buyInAmount"
|
v-model="formData.buyInAmount"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="1000"
|
placeholder="1000"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />
|
@change="autoSave" />
|
||||||
(can be paid over time or waived based on need)
|
(can be paid over time or waived based on need)
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -234,7 +231,7 @@
|
||||||
v-model="formData.noticeDays"
|
v-model="formData.noticeDays"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="30"
|
placeholder="30"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />
|
@change="autoSave" />
|
||||||
days notice. The cooperative will:
|
days notice. The cooperative will:
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -246,7 +243,7 @@
|
||||||
v-model="formData.surplusPayoutDays"
|
v-model="formData.surplusPayoutDays"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="30"
|
placeholder="30"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />
|
@change="autoSave" />
|
||||||
days
|
days
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -256,7 +253,7 @@
|
||||||
v-model="formData.buyInReturnDays"
|
v-model="formData.buyInReturnDays"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="90"
|
placeholder="90"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />
|
@change="autoSave" />
|
||||||
days
|
days
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -276,16 +273,29 @@
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
<!-- Decision Framework Selection -->
|
||||||
<div>
|
<div>
|
||||||
<h3
|
<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
|
Primary Decision Framework
|
||||||
</h3>
|
</h3>
|
||||||
|
<UFormField class="form-group-large mb-4">
|
||||||
|
<USelect
|
||||||
|
v-model="formData.decisionFramework"
|
||||||
|
:items="decisionFrameworkOptions"
|
||||||
|
placeholder="Select decision framework"
|
||||||
|
size="xl"
|
||||||
|
class="w-full"
|
||||||
|
@change="autoSave" />
|
||||||
|
</UFormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
<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
|
getFrameworkDetails(formData.decisionFramework)
|
||||||
cooperative. An objection must explain how the proposal would
|
.practicalDescription
|
||||||
contradict our values or threaten our sustainability.
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -301,7 +311,7 @@
|
||||||
v-model="formData.dayToDayLimit"
|
v-model="formData.dayToDayLimit"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="100"
|
placeholder="100"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />
|
@change="autoSave" />
|
||||||
can be made by any member. Just tell others what you did at
|
can be made by any member. Just tell others what you did at
|
||||||
the next meeting.
|
the next meeting.
|
||||||
|
|
@ -319,13 +329,13 @@
|
||||||
v-model="formData.regularDecisionMin"
|
v-model="formData.regularDecisionMin"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="100"
|
placeholder="100"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />
|
@change="autoSave" />
|
||||||
and $<UInput
|
and $<UInput
|
||||||
v-model="formData.regularDecisionMax"
|
v-model="formData.regularDecisionMax"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="1000"
|
placeholder="1000"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />
|
@change="autoSave" />
|
||||||
need consent from members present at a meeting (minimum 2
|
need consent from members present at a meeting (minimum 2
|
||||||
members).
|
members).
|
||||||
|
|
@ -349,11 +359,14 @@
|
||||||
v-model="formData.majorDebtThreshold"
|
v-model="formData.majorDebtThreshold"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="5000"
|
placeholder="5000"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />
|
@change="autoSave" />
|
||||||
</li>
|
</li>
|
||||||
<li>Fundamental changes to our purpose or structure</li>
|
<li>Fundamental changes to our purpose or structure</li>
|
||||||
<li>Dissolution of the cooperative</li>
|
<li>
|
||||||
|
Dissolution of
|
||||||
|
{{ getDisplayName().toLowerCase() }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -377,7 +390,7 @@
|
||||||
v-model="formData.emergencyNoticeHours"
|
v-model="formData.emergencyNoticeHours"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="24"
|
placeholder="24"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />
|
@change="autoSave" />
|
||||||
hours notice
|
hours notice
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -404,8 +417,9 @@
|
||||||
Equal Ownership
|
Equal Ownership
|
||||||
</h3>
|
</h3>
|
||||||
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
||||||
Each member owns an equal share of the cooperative, regardless
|
Each member owns an equal share of
|
||||||
of hours worked or tenure.
|
{{ getDisplayName().toLowerCase() }},
|
||||||
|
regardless of hours worked or tenure.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -414,7 +428,7 @@
|
||||||
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
|
Paying Ourselves
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- Pay Policy Selection -->
|
<!-- Pay Policy Selection -->
|
||||||
<UFormField label="Pay Policy" class="form-group-large mb-4">
|
<UFormField label="Pay Policy" class="form-group-large mb-4">
|
||||||
<USelect
|
<USelect
|
||||||
|
|
@ -427,15 +441,20 @@
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<!-- Equal Pay Policy -->
|
<!-- Equal Pay Policy -->
|
||||||
<div v-if="formData.payPolicy === 'equal-pay'" class="space-y-3">
|
<div
|
||||||
<p class="content-paragraph">All members receive equal compensation regardless of role or hours worked.</p>
|
v-if="formData.payPolicy === 'equal-pay'"
|
||||||
|
class="space-y-3">
|
||||||
|
<p class="content-paragraph">
|
||||||
|
All members receive equal compensation regardless of role or
|
||||||
|
hours worked.
|
||||||
|
</p>
|
||||||
<ul class="content-list my-2 pl-6 list-disc">
|
<ul class="content-list my-2 pl-6 list-disc">
|
||||||
<li class="flex items-baseline gap-2 flex-wrap">
|
<li class="flex items-baseline gap-2 flex-wrap">
|
||||||
Base rate: $<UInput
|
Base rate: $<UInput
|
||||||
v-model="formData.baseRate"
|
v-model="formData.baseRate"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="25"
|
placeholder="25"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />/hour for all members
|
@change="autoSave" />/hour for all members
|
||||||
</li>
|
</li>
|
||||||
<li class="flex items-baseline gap-2 flex-wrap">
|
<li class="flex items-baseline gap-2 flex-wrap">
|
||||||
|
|
@ -443,7 +462,7 @@
|
||||||
v-model="formData.monthlyDraw"
|
v-model="formData.monthlyDraw"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="2000"
|
placeholder="2000"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />
|
@change="autoSave" />
|
||||||
per member
|
per member
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -451,15 +470,19 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hours-Weighted Policy -->
|
<!-- Hours-Weighted Policy -->
|
||||||
<div v-if="formData.payPolicy === 'hours-weighted'" class="space-y-3">
|
<div
|
||||||
<p class="content-paragraph">Compensation is proportional to hours worked by each member.</p>
|
v-if="formData.payPolicy === 'hours-weighted'"
|
||||||
|
class="space-y-3">
|
||||||
|
<p class="content-paragraph">
|
||||||
|
Compensation is proportional to hours worked by each member.
|
||||||
|
</p>
|
||||||
<ul class="content-list my-2 pl-6 list-disc">
|
<ul class="content-list my-2 pl-6 list-disc">
|
||||||
<li class="flex items-baseline gap-2 flex-wrap">
|
<li class="flex items-baseline gap-2 flex-wrap">
|
||||||
Hourly rate: $<UInput
|
Hourly rate: $<UInput
|
||||||
v-model="formData.hourlyRate"
|
v-model="formData.hourlyRate"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="25"
|
placeholder="25"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />/hour
|
@change="autoSave" />/hour
|
||||||
</li>
|
</li>
|
||||||
<li>Members track their hours and are paid accordingly</li>
|
<li>Members track their hours and are paid accordingly</li>
|
||||||
|
|
@ -468,18 +491,26 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Needs-Weighted Policy -->
|
<!-- Needs-Weighted Policy -->
|
||||||
<div v-if="formData.payPolicy === 'needs-weighted'" class="space-y-3">
|
<div
|
||||||
<p class="content-paragraph">Compensation is allocated based on each member's individual financial needs.</p>
|
v-if="formData.payPolicy === 'needs-weighted'"
|
||||||
|
class="space-y-3">
|
||||||
|
<p class="content-paragraph">
|
||||||
|
Compensation is allocated based on each member's individual
|
||||||
|
financial needs.
|
||||||
|
</p>
|
||||||
<ul class="content-list my-2 pl-6 list-disc">
|
<ul class="content-list my-2 pl-6 list-disc">
|
||||||
<li>Members declare their minimum monthly needs</li>
|
<li>Members declare their minimum monthly needs</li>
|
||||||
<li>Available payroll is distributed proportionally to cover needs</li>
|
<li>
|
||||||
|
Available payroll is distributed proportionally to cover
|
||||||
|
needs
|
||||||
|
</li>
|
||||||
<li>Regular needs assessment and adjustment process</li>
|
<li>Regular needs assessment and adjustment process</li>
|
||||||
<li class="flex items-baseline gap-2 flex-wrap">
|
<li class="flex items-baseline gap-2 flex-wrap">
|
||||||
Minimum guaranteed amount: $<UInput
|
Minimum guaranteed amount: $<UInput
|
||||||
v-model="formData.minGuaranteedPay"
|
v-model="formData.minGuaranteedPay"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="1000"
|
placeholder="1000"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />/month
|
@change="autoSave" />/month
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -487,7 +518,9 @@
|
||||||
|
|
||||||
<!-- Common payment details -->
|
<!-- Common payment details -->
|
||||||
<div class="mt-4 space-y-2">
|
<div class="mt-4 space-y-2">
|
||||||
<p class="content-paragraph font-semibold">Payment Schedule:</p>
|
<p class="content-paragraph font-semibold">
|
||||||
|
Payment Schedule:
|
||||||
|
</p>
|
||||||
<ul class="content-list my-2 pl-6 list-disc">
|
<ul class="content-list my-2 pl-6 list-disc">
|
||||||
<li class="flex items-baseline gap-2 flex-wrap">
|
<li class="flex items-baseline gap-2 flex-wrap">
|
||||||
Paid on the
|
Paid on the
|
||||||
|
|
@ -495,9 +528,9 @@
|
||||||
v-model="formData.paymentDay"
|
v-model="formData.paymentDay"
|
||||||
:items="dayOptions"
|
:items="dayOptions"
|
||||||
placeholder="15th"
|
placeholder="15th"
|
||||||
arrow
|
|
||||||
class="inline-field"
|
class="inline-field"
|
||||||
@change="autoSave" />
|
@change="autoSave" />
|
||||||
|
|
||||||
of each month
|
of each month
|
||||||
</li>
|
</li>
|
||||||
<li class="flex items-baseline gap-2 flex-wrap">
|
<li class="flex items-baseline gap-2 flex-wrap">
|
||||||
|
|
@ -525,7 +558,7 @@
|
||||||
v-model="formData.targetHours"
|
v-model="formData.targetHours"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="40"
|
placeholder="40"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />
|
@change="autoSave" />
|
||||||
(flexible based on capacity)
|
(flexible based on capacity)
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -548,10 +581,7 @@
|
||||||
All members can access all financial records anytime
|
All members can access all financial records anytime
|
||||||
</li>
|
</li>
|
||||||
<li>Monthly financial check-ins at meetings</li>
|
<li>Monthly financial check-ins at meetings</li>
|
||||||
<li>
|
<li>Quarterly reviews of our runway</li>
|
||||||
Quarterly reviews of our runway (how many months we can
|
|
||||||
operate)
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -578,11 +608,11 @@
|
||||||
v-model="formData.roleRotationMonths"
|
v-model="formData.roleRotationMonths"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="6"
|
placeholder="6"
|
||||||
class="inline-field number-field"
|
class="inline-field number-field w-10"
|
||||||
@change="autoSave" />
|
@change="autoSave" />
|
||||||
months. Current roles include:
|
months. Current roles include:
|
||||||
</p>
|
</p>
|
||||||
<UFormField label="Rotating Roles" class="form-group-large">
|
<UFormField class="form-group-large">
|
||||||
<UTextarea
|
<UTextarea
|
||||||
v-model="formData.rotatingRoles"
|
v-model="formData.rotatingRoles"
|
||||||
:rows="4"
|
:rows="4"
|
||||||
|
|
@ -602,7 +632,7 @@
|
||||||
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
||||||
All members participate in:
|
All members participate in:
|
||||||
</p>
|
</p>
|
||||||
<UFormField label="Shared Responsibilities" class="form-group-large">
|
<UFormField class="form-group-large">
|
||||||
<UTextarea
|
<UTextarea
|
||||||
v-model="formData.sharedResponsibilities"
|
v-model="formData.sharedResponsibilities"
|
||||||
:rows="3"
|
:rows="3"
|
||||||
|
|
@ -669,8 +699,13 @@
|
||||||
placeholder="year"
|
placeholder="year"
|
||||||
class="inline-field"
|
class="inline-field"
|
||||||
@change="autoSave" />
|
@change="autoSave" />
|
||||||
and update it through our consent process. Small clarifications
|
and update it through our
|
||||||
can happen anytime; structural changes need full member consent.
|
<span class="font-semibold">{{
|
||||||
|
getFrameworkLabel(formData.decisionFramework)
|
||||||
|
}}</span>
|
||||||
|
process. Small clarifications can happen anytime; structural
|
||||||
|
changes need
|
||||||
|
{{ getStructuralChangeRequirement(formData.decisionFramework) }}.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -683,7 +718,8 @@
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
||||||
If the cooperative dissolves:
|
If
|
||||||
|
{{ getDisplayName().toLowerCase() }} dissolves:
|
||||||
</p>
|
</p>
|
||||||
<ol class="content-list numbered my-2 pl-6 list-decimal">
|
<ol class="content-list numbered my-2 pl-6 list-decimal">
|
||||||
<li>Pay all debts and obligations</li>
|
<li>Pay all debts and obligations</li>
|
||||||
|
|
@ -700,21 +736,33 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Section 9: Legal Bits -->
|
<!-- Section 9: Legal Registration (Optional) -->
|
||||||
<div class="section-card">
|
<div class="section-card">
|
||||||
<h2
|
<div class="flex items-center justify-between mb-4">
|
||||||
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-4">
|
<h2
|
||||||
9. Legal Bits
|
class="section-title text-2xl font-extrabold text-neutral-900 dark:text-neutral-100 mb-0">
|
||||||
</h2>
|
9. Legal Registration
|
||||||
|
</h2>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label
|
||||||
|
class="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
|
Legally registered?
|
||||||
|
</label>
|
||||||
|
<USwitch
|
||||||
|
v-model="formData.isLegallyRegistered"
|
||||||
|
@change="autoSave" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div v-if="formData.isLegallyRegistered" class="space-y-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<UFormField label="Legal Structure" class="form-group-block">
|
<UFormField label="Legal Structure" class="form-group-block">
|
||||||
<UInput
|
<UInput
|
||||||
v-model="formData.legalStructure"
|
v-model="formData.legalStructure"
|
||||||
size="xl"
|
size="xl"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
placeholder="Cooperative corporation, LLC, partnership, etc." />
|
placeholder="Cooperative corporation, LLC, partnership, etc."
|
||||||
|
@change="autoSave" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<UFormField label="Registered in" class="form-group-inline">
|
<UFormField label="Registered in" class="form-group-inline">
|
||||||
|
|
@ -754,8 +802,16 @@
|
||||||
but work to align our legal structure with our values.
|
but work to align our legal structure with our values.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div v-else class="text-neutral-600 dark:text-neutral-400 italic">
|
||||||
|
<p class="content-paragraph">
|
||||||
|
{{ getDisplayName() || "This cooperative" }} operates as
|
||||||
|
an informal collective. If we decide to register legally in the
|
||||||
|
future, we'll update this section with our legal structure
|
||||||
|
details.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -771,6 +827,9 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
|
// Import centralized coop info
|
||||||
|
const { coopInfo, updateCoopInfo, getDisplayName } = useCoopInfo();
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: false,
|
layout: false,
|
||||||
});
|
});
|
||||||
|
|
@ -795,40 +854,194 @@ const monthOptions = [
|
||||||
|
|
||||||
const dayOptions = Array.from({ length: 31 }, (_, i) => ({
|
const dayOptions = Array.from({ length: 31 }, (_, i) => ({
|
||||||
value: i + 1,
|
value: i + 1,
|
||||||
label: `${i + 1}${getOrdinalSuffix(i + 1)}`
|
label: `${i + 1}${getOrdinalSuffix(i + 1)}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Helper function to get ordinal suffix (1st, 2nd, 3rd, etc.)
|
// Helper function to get ordinal suffix (1st, 2nd, 3rd, etc.)
|
||||||
function getOrdinalSuffix(num) {
|
function getOrdinalSuffix(num) {
|
||||||
if (num >= 11 && num <= 13) {
|
if (num >= 11 && num <= 13) {
|
||||||
return 'th';
|
return "th";
|
||||||
}
|
}
|
||||||
switch (num % 10) {
|
switch (num % 10) {
|
||||||
case 1: return 'st';
|
case 1:
|
||||||
case 2: return 'nd';
|
return "st";
|
||||||
case 3: return 'rd';
|
case 2:
|
||||||
default: return 'th';
|
return "nd";
|
||||||
|
case 3:
|
||||||
|
return "rd";
|
||||||
|
default:
|
||||||
|
return "th";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const payPolicyOptions = [
|
const payPolicyOptions = [
|
||||||
{ value: 'equal-pay', label: 'Equal Pay - All members receive equal compensation' },
|
{
|
||||||
{ value: 'hours-weighted', label: 'Hours-Weighted - Pay proportional to hours worked' },
|
value: "equal-pay",
|
||||||
{ value: 'needs-weighted', label: 'Needs-Weighted - Pay proportional to individual needs' }
|
label: "Equal Pay - All members receive equal compensation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "hours-weighted",
|
||||||
|
label: "Hours-Weighted - Pay proportional to hours worked",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "needs-weighted",
|
||||||
|
label: "Needs-Weighted - Pay proportional to individual needs",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const decisionFrameworkOptions = [
|
||||||
|
{
|
||||||
|
value: "consent-based",
|
||||||
|
label: "Consent-Based - No one objects strongly enough to block",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "consensus",
|
||||||
|
label: "Full Consensus - Everyone agrees to support",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "consultative",
|
||||||
|
label: "Consultative - Gather input, then designated person decides",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "democratic-vote",
|
||||||
|
label: "Democratic Vote - Majority decides",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "advice-process",
|
||||||
|
label: "Advice Process - Decision-maker seeks input, then decides",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "delegation",
|
||||||
|
label: "Delegation - Empower responsible party to decide",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "defer-to-expert",
|
||||||
|
label: "Defer to Expert - Trust the person who knows best",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "facilitated-discussion",
|
||||||
|
label: "Facilitated Discussion - Talk it through with structure",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Helper function to get framework label
|
||||||
|
function getFrameworkLabel(framework) {
|
||||||
|
const labels = {
|
||||||
|
"consent-based": "consent-based decision",
|
||||||
|
consensus: "consensus",
|
||||||
|
consultative: "consultative",
|
||||||
|
"democratic-vote": "democratic voting",
|
||||||
|
"advice-process": "advice",
|
||||||
|
delegation: "delegation",
|
||||||
|
"defer-to-expert": "expert-led",
|
||||||
|
"facilitated-discussion": "facilitated discussion",
|
||||||
|
};
|
||||||
|
return labels[framework] || "decision-making";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get structural change requirement text
|
||||||
|
function getStructuralChangeRequirement(framework) {
|
||||||
|
const requirements = {
|
||||||
|
"consent-based": "no blocking objections from any member",
|
||||||
|
consensus: "full consensus from all members",
|
||||||
|
consultative: "consultation with all members before the decision",
|
||||||
|
"democratic-vote": "a majority vote of all members",
|
||||||
|
"advice-process": "advice from all members before the decision",
|
||||||
|
delegation: "approval from the delegated authority",
|
||||||
|
"defer-to-expert":
|
||||||
|
"approval from the designated expert after member consultation",
|
||||||
|
"facilitated-discussion": "a facilitated discussion with all members",
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
requirements[framework] || "approval through our chosen decision process"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get framework details
|
||||||
|
function getFrameworkDetails(framework) {
|
||||||
|
const details = {
|
||||||
|
"consent-based": {
|
||||||
|
tagline: "No one objects strongly enough to block",
|
||||||
|
description:
|
||||||
|
"Not everyone needs to love it, but no one sees it as harmful to our organization. Focus on addressing objections rather than optimizing preferences.",
|
||||||
|
practicalDescription:
|
||||||
|
"We use consent, not consensus. This means we move forward when no one has a principled objection that would harm our organization. An objection must explain how the proposal would contradict our values or threaten our sustainability.",
|
||||||
|
},
|
||||||
|
consensus: {
|
||||||
|
tagline: "Everyone agrees to support the decision",
|
||||||
|
description:
|
||||||
|
"Take the time to get real alignment on high-stakes decisions. Everyone can live with it, even if it's not their favorite option.",
|
||||||
|
practicalDescription:
|
||||||
|
"For major decisions, we work together until everyone can support the outcome. This doesn't mean it's everyone's first choice, but that everyone understands and commits to the decision.",
|
||||||
|
},
|
||||||
|
consultative: {
|
||||||
|
tagline: "Gather input, then designated person decides",
|
||||||
|
description:
|
||||||
|
"When no one has clear expertise but we need various perspectives, one person gathers input and makes the final call.",
|
||||||
|
practicalDescription:
|
||||||
|
"A designated decision owner seeks input from all stakeholders, researches options, and makes the decision with clear reasoning. They explain how input influenced the final decision.",
|
||||||
|
},
|
||||||
|
"democratic-vote": {
|
||||||
|
tagline: "Majority decides, move forward together",
|
||||||
|
description:
|
||||||
|
"For larger groups or time-sensitive decisions, voting provides clear resolution while respecting everyone's input.",
|
||||||
|
practicalDescription:
|
||||||
|
"After discussion, we vote and move forward with the majority decision. We document minority concerns and revisit them if needed. Anonymous voting reduces peer pressure.",
|
||||||
|
},
|
||||||
|
"advice-process": {
|
||||||
|
tagline: "Decision-maker seeks input, then decides",
|
||||||
|
description:
|
||||||
|
"Balances inclusion with efficiency. The decision owner genuinely considers input but isn't bound by it.",
|
||||||
|
practicalDescription:
|
||||||
|
"The person most affected or willing becomes the decision owner. They seek advice from those with expertise and those affected, then make the decision and explain their reasoning.",
|
||||||
|
},
|
||||||
|
delegation: {
|
||||||
|
tagline: "Empower the responsible party to decide",
|
||||||
|
description:
|
||||||
|
"Trust those closest to the work to make decisions within clear scope and constraints.",
|
||||||
|
practicalDescription:
|
||||||
|
"We delegate decisions to the people most affected or with the most expertise. They have authority within defined boundaries and report back on outcomes.",
|
||||||
|
},
|
||||||
|
"defer-to-expert": {
|
||||||
|
tagline: "Trust the person who knows this best",
|
||||||
|
description:
|
||||||
|
"When someone has clear expertise, let them lead while keeping everyone informed.",
|
||||||
|
practicalDescription:
|
||||||
|
"The expert proposes solutions with reasoning, answers clarifying questions, and makes the final call. They explain their thinking, not just the outcome.",
|
||||||
|
},
|
||||||
|
"facilitated-discussion": {
|
||||||
|
tagline: "Talk it through with structure",
|
||||||
|
description:
|
||||||
|
"Use structured discussion to find clarity before choosing a specific decision method.",
|
||||||
|
practicalDescription:
|
||||||
|
"We clarify what we're deciding, share all relevant information, and each person shares their perspective with time limits. We identify alignment and differences, then choose the appropriate method.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
details[framework] || {
|
||||||
|
tagline: "Select a framework to see details",
|
||||||
|
description: "",
|
||||||
|
practicalDescription:
|
||||||
|
"Choose a decision-making framework above to see how it works in practice.",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
cooperativeName: "",
|
cooperativeName: "",
|
||||||
dateEstablished: "",
|
dateEstablished: "",
|
||||||
purpose: "",
|
purpose: "",
|
||||||
coreValues: "",
|
coreValues: "",
|
||||||
memberRequirements: "Shares our values and purpose\nContributes labour to the cooperative (by doing actual work, not just investing money)\nCommits to collective decision-making\nParticipates in governance responsibilities",
|
memberRequirements:
|
||||||
|
"Shares our values and purpose\nContributes labour to our organization (by doing actual work, not just investing money)\nCommits to collective decision-making\nParticipates in governance responsibilities",
|
||||||
members: [{ name: "", email: "", joinDate: "", role: "" }],
|
members: [{ name: "", email: "", joinDate: "", role: "" }],
|
||||||
trialPeriodMonths: 3,
|
trialPeriodMonths: 3,
|
||||||
buyInAmount: "",
|
buyInAmount: "",
|
||||||
noticeDays: 30,
|
noticeDays: 30,
|
||||||
surplusPayoutDays: 30,
|
surplusPayoutDays: 30,
|
||||||
buyInReturnDays: 90,
|
buyInReturnDays: 90,
|
||||||
|
decisionFramework: "consent-based", // Default to consent-based
|
||||||
dayToDayLimit: 100,
|
dayToDayLimit: 100,
|
||||||
regularDecisionMin: 100,
|
regularDecisionMin: 100,
|
||||||
regularDecisionMax: 1000,
|
regularDecisionMax: 1000,
|
||||||
|
|
@ -845,10 +1058,13 @@ const formData = ref({
|
||||||
surplusFrequency: "quarter",
|
surplusFrequency: "quarter",
|
||||||
targetHours: 40,
|
targetHours: 40,
|
||||||
roleRotationMonths: 6,
|
roleRotationMonths: 6,
|
||||||
rotatingRoles: "Financial coordinator (handles bookkeeping, not financial decisions)\nMeeting facilitator\nExternal communications\nOthers",
|
rotatingRoles:
|
||||||
sharedResponsibilities: "Governance and decision-making\nStrategic planning\nMutual support and care",
|
"Financial coordinator (handles bookkeeping, not financial decisions)\nMeeting facilitator\nExternal communications\nOthers",
|
||||||
|
sharedResponsibilities:
|
||||||
|
"Governance and decision-making\nStrategic planning\nMutual support and care",
|
||||||
reviewFrequency: "year",
|
reviewFrequency: "year",
|
||||||
assetDonationTarget: "",
|
assetDonationTarget: "",
|
||||||
|
isLegallyRegistered: false,
|
||||||
legalStructure: "",
|
legalStructure: "",
|
||||||
registeredLocation: "",
|
registeredLocation: "",
|
||||||
fiscalYearEndMonth: "December",
|
fiscalYearEndMonth: "December",
|
||||||
|
|
@ -872,6 +1088,35 @@ const loadSavedData = () => {
|
||||||
// Load data immediately
|
// Load data immediately
|
||||||
loadSavedData();
|
loadSavedData();
|
||||||
|
|
||||||
|
// Sync with centralized coop info
|
||||||
|
watch(
|
||||||
|
() => coopInfo.value,
|
||||||
|
(newCoopInfo) => {
|
||||||
|
formData.value.cooperativeName = newCoopInfo.cooperativeName || formData.value.cooperativeName;
|
||||||
|
formData.value.dateEstablished = newCoopInfo.dateEstablished || formData.value.dateEstablished;
|
||||||
|
formData.value.purpose = newCoopInfo.purpose || formData.value.purpose;
|
||||||
|
formData.value.coreValues = newCoopInfo.coreValues || formData.value.coreValues;
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update centralized store when key fields change
|
||||||
|
watch(
|
||||||
|
() => ({
|
||||||
|
cooperativeName: formData.value.cooperativeName,
|
||||||
|
dateEstablished: formData.value.dateEstablished,
|
||||||
|
purpose: formData.value.purpose,
|
||||||
|
coreValues: formData.value.coreValues,
|
||||||
|
legalStructure: formData.value.legalStructure,
|
||||||
|
registeredLocation: formData.value.registeredLocation,
|
||||||
|
isLegallyRegistered: formData.value.isLegallyRegistered,
|
||||||
|
}),
|
||||||
|
(newData) => {
|
||||||
|
updateCoopInfo(newData);
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
// Auto-save to localStorage (removed immediate to prevent overwriting)
|
// Auto-save to localStorage (removed immediate to prevent overwriting)
|
||||||
watch(
|
watch(
|
||||||
formData,
|
formData,
|
||||||
|
|
@ -1072,9 +1317,30 @@ const exportData = computed(() => ({
|
||||||
// Pass the complete formData object - this is what the export functions use
|
// Pass the complete formData object - this is what the export functions use
|
||||||
formData: formData.value,
|
formData: formData.value,
|
||||||
// Also provide direct access to key fields for backward compatibility
|
// Also provide direct access to key fields for backward compatibility
|
||||||
cooperativeName: formData.value.cooperativeName || "Worker Cooperative",
|
cooperativeName: getDisplayName(),
|
||||||
section: "membership-agreement",
|
section: "membership-agreement",
|
||||||
exportedAt: new Date().toISOString(),
|
exportedAt: new Date().toISOString(),
|
||||||
|
// Include computed/dynamic values for complete export
|
||||||
|
decisionFrameworkDetails: getFrameworkDetails(
|
||||||
|
formData.value.decisionFramework
|
||||||
|
),
|
||||||
|
decisionFrameworkLabel: getFrameworkLabel(formData.value.decisionFramework),
|
||||||
|
structuralChangeRequirement: getStructuralChangeRequirement(
|
||||||
|
formData.value.decisionFramework
|
||||||
|
),
|
||||||
|
paymentDayLabel: formData.value.paymentDay
|
||||||
|
? `${formData.value.paymentDay}${getOrdinalSuffix(
|
||||||
|
formData.value.paymentDay
|
||||||
|
)}`
|
||||||
|
: "15th",
|
||||||
|
// Include selected dropdown labels for readability
|
||||||
|
decisionFrameworkName:
|
||||||
|
decisionFrameworkOptions.find(
|
||||||
|
(opt) => opt.value === formData.value.decisionFramework
|
||||||
|
)?.label || "Consent-Based - No one objects strongly enough to block",
|
||||||
|
payPolicyName:
|
||||||
|
payPolicyOptions.find((opt) => opt.value === formData.value.payPolicy)
|
||||||
|
?.label || "Equal Pay - All members receive equal compensation",
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue