app/components/ExportOptions.vue

1216 lines
50 KiB
Vue

<template>
<div class="export-options" :class="containerClass">
<div class="export-content">
<div class="export-section">
<div class="export-buttons">
<UButton
@click="copyToClipboard"
class="export-btn"
:disabled="isProcessing"
ref="copyButton"
>
<UIcon name="i-heroicons-clipboard" />
<span>Copy as Text</span>
<UIcon v-if="showCopySuccess" name="i-heroicons-check" class="success-icon" />
</UButton>
<UButton
@click="downloadAsMarkdown"
class="export-btn"
:disabled="isProcessing"
ref="downloadButton"
>
<UIcon name="i-heroicons-arrow-down-tray" />
<span>Download Markdown</span>
<UIcon
v-if="showDownloadSuccess"
name="i-heroicons-check"
class="success-icon"
/>
</UButton>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
exportData: any;
filename?: string;
title?: string;
containerClass?: string;
}
const props = withDefaults(defineProps<Props>(), {
filename: "export",
title: "Export Data",
containerClass: "centered",
});
const isProcessing = ref(false);
const showCopySuccess = ref(false);
const showDownloadSuccess = ref(false);
const copyButton = ref<HTMLButtonElement>();
const downloadButton = ref<HTMLButtonElement>();
// Success feedback animation
const showSuccessFeedback = (
buttonRef: Ref<HTMLButtonElement | undefined>,
successRef: Ref<boolean>
) => {
try {
successRef.value = true;
// Add checkmark overlay animation if button exists
if (buttonRef?.value && typeof buttonRef.value.classList?.add === 'function') {
const button = buttonRef.value;
button.classList.add("success-state");
setTimeout(() => {
try {
if (button && typeof button.classList?.remove === 'function') {
button.classList.remove("success-state");
}
} catch (e) {
console.warn('Could not remove success-state class:', e);
}
}, 2000);
}
// Reset success state
setTimeout(() => {
try {
successRef.value = false;
} catch (e) {
console.warn('Could not reset success state:', e);
}
}, 2000);
} catch (error) {
console.warn('Success feedback failed:', error);
// Still show success state even if animation fails
successRef.value = true;
setTimeout(() => {
try {
successRef.value = false;
} catch (e) {
// Ignore
}
}, 2000);
}
};
const copyToClipboard = async () => {
if (isProcessing.value) return;
isProcessing.value = true;
try {
const textContent = extractTextContent();
// Modern clipboard API
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(textContent);
try {
showSuccessFeedback(copyButton, showCopySuccess);
} catch (feedbackError) {
console.warn("Success feedback failed, but copy succeeded:", feedbackError);
}
return;
} catch (clipboardError) {
console.warn("Modern clipboard failed, trying fallback:", clipboardError);
}
}
// Fallback method
const textarea = document.createElement("textarea");
textarea.value = textContent;
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
textarea.style.top = "-9999px";
textarea.setAttribute('readonly', '');
document.body.appendChild(textarea);
// Select the text
textarea.select();
textarea.setSelectionRange(0, 99999); // For mobile devices
let copySucceeded = false;
try {
const successful = document.execCommand("copy");
copySucceeded = successful;
if (!successful) {
throw new Error("execCommand copy returned false");
}
} catch (execError) {
console.error("execCommand failed:", execError);
throw new Error("Both clipboard methods failed");
} finally {
try {
document.body.removeChild(textarea);
} catch (cleanupError) {
console.warn("Could not clean up textarea:", cleanupError);
}
}
// Show success feedback only if copy actually succeeded
if (copySucceeded) {
try {
showSuccessFeedback(copyButton, showCopySuccess);
} catch (feedbackError) {
console.warn("Success feedback failed, but copy succeeded:", feedbackError);
}
}
} catch (error) {
console.error("Copy failed:", error);
alert("Copy failed. Please try the Download Markdown button instead.");
} finally {
isProcessing.value = false;
}
};
const downloadAsMarkdown = () => {
if (isProcessing.value) return;
isProcessing.value = true;
try {
const content = convertToMarkdown();
downloadFile(content, `${props.filename}.md`, "text/markdown");
showSuccessFeedback(downloadButton, showDownloadSuccess);
} catch (error) {
console.error("Markdown download failed:", error);
alert("Download failed. Please try again.");
} finally {
isProcessing.value = false;
}
};
const extractTextContent = (): string => {
const today = new Date().toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
let content = `${props.title.toUpperCase()}\n${"=".repeat(
props.title.length
)}\n\nExported ${today}\n\n`;
// Convert data to readable text format
if (props.exportData) {
content += formatDataAsText(props.exportData);
}
return content;
};
const convertToMarkdown = (): string => {
const today = new Date().toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
let content = `# ${props.title}\n\n*Exported ${today}*\n\n`;
// Convert data to markdown format
if (props.exportData) {
content += formatDataAsMarkdown(props.exportData);
}
return content;
};
const formatDataAsText = (data: any): string => {
// Special handling for different template types
if (data.section === "tech-charter") {
return formatTechCharterAsText(data);
} else if (data.section === "membership-agreement") {
return formatMembershipAgreementAsText(data);
} else if (data.section === "conflict-resolution-framework") {
return formatConflictResolutionAsText(data);
} else if (data.section === "decision-framework") {
return formatDecisionFrameworkAsText(data);
}
if (Array.isArray(data)) {
return data
.map((item, index) => `${index + 1}. ${formatObjectAsText(item)}`)
.join("\n");
} else if (typeof data === "object" && data !== null) {
return formatObjectAsText(data);
}
return String(data);
};
const formatDataAsMarkdown = (data: any): string => {
// Special handling for different template types
if (data.section === "tech-charter") {
return formatTechCharterAsMarkdown(data);
} else if (data.section === "membership-agreement") {
return formatMembershipAgreementAsMarkdown(data);
} else if (data.section === "conflict-resolution-framework") {
return formatConflictResolutionAsMarkdown(data);
} else if (data.section === "decision-framework") {
return formatDecisionFrameworkAsMarkdown(data);
}
if (Array.isArray(data)) {
return data
.map((item, index) => `${index + 1}. ${formatObjectAsMarkdown(item)}`)
.join("\n\n");
} else if (typeof data === "object" && data !== null) {
return formatObjectAsMarkdown(data);
}
return String(data);
};
const formatTechCharterAsText = (data: any): string => {
let content = `PURPOSE\n-------\n\n${data.charterPurpose || '[No purpose defined]'}\n\n`;
const principleWeights = data.principleWeights || {};
const nonNegotiables = data.nonNegotiables || [];
const principles = data.principles || [];
const constraints = data.constraints || {};
const sortedWeights = data.sortedWeights || [];
const selectedPrinciples = Object.keys(principleWeights).filter(
(p) => principleWeights[p] > 0
);
if (selectedPrinciples.filter((p) => !nonNegotiables.includes(p)).length > 0) {
content += `CORE PRINCIPLES\n---------------\n\n`;
selectedPrinciples
.filter((p) => !nonNegotiables.includes(p))
.forEach((pId) => {
const principle = principles.find((p: any) => p.id === pId);
if (principle) {
content += `${principle.name}\n`;
}
});
content += "\n";
}
if (nonNegotiables.length > 0) {
content += `NON-NEGOTIABLE REQUIREMENTS\n---------------------------\n\nAny vendor failing these requirements is automatically disqualified.\n\n`;
nonNegotiables.forEach((pId: string) => {
const principle = principles.find((p: any) => p.id === pId);
if (principle) {
content += `${principle.name}\n`;
}
});
content += "\n";
}
content += `TECHNICAL CONSTRAINTS\n---------------------\n\n`;
content += `• Authentication: ${constraints.sso || 'Not specified'}\n`;
content += `• Hosting: ${constraints.hosting || 'Not specified'}\n`;
if (constraints.integrations && Array.isArray(constraints.integrations) && constraints.integrations.length > 0) {
content += `• Required Integrations: ${constraints.integrations.join(", ")}\n`;
}
content += `• Support Level: ${constraints.support || 'Not specified'}\n`;
content += `• Migration Timeline: ${constraints.timeline || 'Not specified'}\n\n`;
if (sortedWeights.length > 0) {
content += `EVALUATION RUBRIC\n-----------------\n\nScore each vendor option using these weighted criteria (0-5 scale):\n\n`;
sortedWeights.forEach((principle: any) => {
content += `${principle.name} (Weight: ${principleWeights[principle.id] || 0})\n${
principle.rubricDescription || 'No description provided'
}\n\n`;
});
}
return content;
};
const formatTechCharterAsMarkdown = (data: any): string => {
let content = `## Purpose\n\n${data.charterPurpose || '[No purpose defined]'}\n\n`;
const principleWeights = data.principleWeights || {};
const nonNegotiables = data.nonNegotiables || [];
const principles = data.principles || [];
const constraints = data.constraints || {};
const sortedWeights = data.sortedWeights || [];
const selectedPrinciples = Object.keys(principleWeights).filter(
(p) => principleWeights[p] > 0
);
if (selectedPrinciples.filter((p) => !nonNegotiables.includes(p)).length > 0) {
content += `## Core Principles\n\n`;
selectedPrinciples
.filter((p) => !nonNegotiables.includes(p))
.forEach((pId) => {
const principle = principles.find((p: any) => p.id === pId);
if (principle) {
content += `- ${principle.name}\n`;
}
});
content += "\n";
}
if (nonNegotiables.length > 0) {
content += `## Non-Negotiable Requirements\n\n**Any vendor failing these requirements is automatically disqualified.**\n\n`;
nonNegotiables.forEach((pId: string) => {
const principle = principles.find((p: any) => p.id === pId);
if (principle) {
content += `- **${principle.name}**\n`;
}
});
content += "\n";
}
content += `## Technical Constraints\n\n`;
content += `- Authentication: ${constraints.sso || 'Not specified'}\n`;
content += `- Hosting: ${constraints.hosting || 'Not specified'}\n`;
if (constraints.integrations && Array.isArray(constraints.integrations) && constraints.integrations.length > 0) {
content += `- Required Integrations: ${constraints.integrations.join(", ")}\n`;
}
content += `- Support Level: ${constraints.support || 'Not specified'}\n`;
content += `- Migration Timeline: ${constraints.timeline || 'Not specified'}\n\n`;
if (sortedWeights.length > 0) {
content += `## Evaluation Rubric\n\nScore each vendor option using these weighted criteria (0-5 scale):\n\n`;
content += `| Criterion | Description | Weight |\n`;
content += `|-----------|-------------|--------|\n`;
sortedWeights.forEach((principle: any) => {
content += `| ${principle.name || 'Unknown'} | ${principle.rubricDescription || 'No description'} | ${
principleWeights[principle.id] || 0
} |\n`;
});
}
return content;
};
const formatObjectAsText = (obj: any): string => {
if (!obj || typeof obj !== "object") return String(obj);
return Object.entries(obj)
.filter(([key, value]) => value !== null && value !== undefined && key !== "id")
.map(([key, value]) => {
const formattedKey = key
.replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => str.toUpperCase());
if (typeof value === "object") {
return `${formattedKey}: ${JSON.stringify(value)}`;
}
return `${formattedKey}: ${value}`;
})
.join("\n");
};
const formatObjectAsMarkdown = (obj: any): string => {
if (!obj || typeof obj !== "object") return String(obj);
return Object.entries(obj)
.filter(([key, value]) => value !== null && value !== undefined && key !== "id")
.map(([key, value]) => {
const formattedKey = key
.replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => str.toUpperCase());
if (typeof value === "object") {
return `**${formattedKey}**: \`${JSON.stringify(value)}\``;
}
return `**${formattedKey}**: ${value}`;
})
.join(" \n");
};
// Membership Agreement formatting - Complete document with all static and dynamic content
const formatMembershipAgreementAsText = (data: any): string => {
const formData = data.formData || {};
const cooperativeName = data.cooperativeName || formData.cooperativeName || "the cooperative";
let content = "";
// Section 1: Who We Are
content += `1. WHO WE ARE\n-------------\n\n`;
content += `Cooperative Name: ${formData.cooperativeName || "[Enter cooperative name]"}\n`;
content += `Date Established: ${formData.dateEstablished || "[Enter date]"}\n\n`;
content += `Our Purpose:\n${formData.purpose || "[Write 1-2 sentences about what your cooperative does and why it exists]"}\n\n`;
content += `Our Core Values:\n${formData.coreValues || "[List 3-5 values that guide everything you do. Examples: mutual care, creative sustainability, economic justice, collective liberation]"}\n\n`;
content += `Current Members:\n`;
if (formData.members && formData.members.length > 0) {
formData.members.forEach((member: any, index: number) => {
content += `${index + 1}. ${member.name || "[Name]"}`;
if (member.email) content += ` (${member.email})`;
if (member.joinDate) content += ` - Joined: ${member.joinDate}`;
if (member.role) content += ` - Role: ${member.role}`;
content += `\n`;
});
} else {
content += `1. [Member Name] ([Email]) - Joined: [Date] - Role: [Optional Role]\n`;
}
content += `\n`;
// Section 2: Membership
content += `2. MEMBERSHIP\n-------------\n\n`;
content += `Who Can Be a Member:\nAny person who:\n\n`;
content += `${formData.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"}\n\n`;
content += `Becoming a Member:\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 += `2. Values alignment conversation\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 += `Leaving the Cooperative:\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 += `• Return their buy-in contribution within ${formData.buyInReturnDays || 90} days\n`;
content += `• Maintain respectful ongoing relationships when possible\n\n`;
// Section 3: How We Make Decisions
content += `3. HOW WE MAKE DECISIONS\n------------------------\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 += `Regular Decisions:\nDecisions between $${formData.regularDecisionMin || 100} and $${formData.regularDecisionMax || 1000} need consent from members present at a meeting (minimum 2 members).\n\n`;
content += `Major Decisions:\nThese require consent from all members:\n`;
content += `• Adding or removing members\n`;
content += `• Changing this agreement\n`;
content += `• Taking on debt over $${formData.majorDebtThreshold || 5000}\n`;
content += `• Fundamental changes to our purpose or structure\n`;
content += `• Dissolution of ${cooperativeName}\n\n`;
content += `Meeting Structure:\n`;
content += `• Regular meetings happen ${formData.meetingFrequency || "weekly"}\n`;
content += `• Emergency meetings need ${formData.emergencyNoticeHours || 24} hours notice\n`;
content += `• We rotate who facilitates meetings\n`;
content += `• Decisions and reasoning get documented in shared notes\n\n`;
// Section 4: Money and Labour
content += `4. MONEY AND LABOUR\n-------------------\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`;
const payPolicy = formData.payPolicy || "equal-pay";
const payPolicyName = payPolicy.replace('-', ' ').replace(/\b\w/g, (l: string) => l.toUpperCase());
content += `Pay Policy: ${data.payPolicyName || payPolicyName}\n\n`;
if (payPolicy === 'equal-pay') {
content += `All members receive equal compensation regardless of role or hours worked.\n`;
if (formData.baseRate) content += `• Base rate: $${formData.baseRate}/hour for all members\n`;
if (formData.monthlyDraw) content += `• Equal monthly draw of $${formData.monthlyDraw} per member\n`;
} else if (payPolicy === 'hours-weighted') {
content += `Compensation is proportional to hours worked by each member.\n`;
content += `• Hourly rate: $${formData.hourlyRate || 25}/hour\n`;
content += `• Members track their hours and are paid accordingly\n`;
content += `• Minimum hours commitment may apply\n`;
} else if (payPolicy === 'needs-weighted') {
content += `Compensation is allocated based on each member's individual financial needs.\n`;
content += `• Members declare their minimum monthly needs\n`;
content += `• Available payroll is distributed proportionally to cover needs\n`;
content += `• Regular needs assessment and adjustment process\n`;
content += `• Minimum guaranteed amount: $${formData.minGuaranteedPay || 1000}/month\n`;
}
content += `\nPayment Schedule:\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 += `Work Expectations:\n`;
content += `• Target hours per week: ${formData.targetHours || 40} (flexible based on capacity)\n`;
content += `• We explicitly reject crunch culture\n`;
content += `• Members communicate their capacity openly\n`;
content += `• We adjust workload collectively when someone needs reduced hours\n\n`;
content += `Financial Transparency:\n`;
content += `• All members can access all financial records anytime\n`;
content += `• Monthly financial check-ins at meetings\n`;
content += `• Quarterly reviews of our runway (how many months we can operate)\n\n`;
// Section 5: Roles and Responsibilities
content += `5. ROLES AND RESPONSIBILITIES\n-----------------------------\n\n`;
content += `Rotating Roles:\n`;
content += `We rotate operational roles every ${formData.roleRotationMonths || 6} months. Current roles include:\n\n`;
content += `${formData.rotatingRoles || "Financial coordinator (handles bookkeeping, not financial decisions)\nMeeting facilitator\nExternal communications\nOthers"}\n\n`;
content += `Shared Responsibilities:\n`;
content += `All members participate in:\n\n`;
content += `${formData.sharedResponsibilities || "Governance and decision-making\nStrategic planning\nMutual support and care"}\n\n`;
// Section 6: Conflict and Care
content += `6. CONFLICT AND CARE\n--------------------\n\n`;
content += `When Conflict Happens:\n`;
content += `1. Direct conversation between parties (if comfortable)\n`;
content += `2. Mediation with another member\n`;
content += `3. Full group discussion with structured process\n`;
content += `4. External mediation if needed\n\n`;
content += `Care Commitments:\n`;
content += `• We check in about capacity and wellbeing regularly\n`;
content += `• We honour diverse access needs\n`;
content += `• We maintain flexibility for life circumstances\n`;
content += `• We contribute to mutual aid when members face hardship\n\n`;
// Section 7: Changing This Agreement
content += `7. CHANGING THIS AGREEMENT\n--------------------------\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
content += `8. IF WE NEED TO CLOSE\n----------------------\n\n`;
content += `If ${cooperativeName} dissolves:\n`;
content += `1. Pay all debts and obligations\n`;
content += `2. Return member contributions\n`;
content += `3. Distribute remaining assets equally among members\n`;
if (formData.assetDonationTarget) {
content += `4. Or donate remaining assets to ${formData.assetDonationTarget}\n`;
}
content += `\n`;
// Section 9: Legal Registration
content += `9. LEGAL REGISTRATION\n---------------------\n\n`;
if (formData.isLegallyRegistered) {
content += `Legal Structure: ${formData.legalStructure || "[Cooperative corporation, LLC, partnership, etc.]"}\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;
};
const formatMembershipAgreementAsMarkdown = (data: any): string => {
const formData = data.formData || {};
const cooperativeName = data.cooperativeName || formData.cooperativeName || "the cooperative";
let content = "";
// Section 1: Who We Are
content += `## 1. Who We Are\n\n`;
content += `**Cooperative Name:** ${formData.cooperativeName || "[Enter cooperative name]"} \n`;
content += `**Date Established:** ${formData.dateEstablished || "[Enter date]"} \n\n`;
content += `**Our Purpose:** \n${formData.purpose || "[Write 1-2 sentences about what your cooperative does and why it exists]"}\n\n`;
content += `**Our Core Values:** \n${formData.coreValues || "[List 3-5 values that guide everything you do. Examples: mutual care, creative sustainability, economic justice, collective liberation]"}\n\n`;
content += `**Current Members:**\n\n`;
if (formData.members && formData.members.length > 0) {
formData.members.forEach((member: any, index: number) => {
content += `${index + 1}. **${member.name || "[Name]"}**`;
if (member.email) content += ` (${member.email})`;
if (member.joinDate) content += ` - *Joined: ${member.joinDate}*`;
if (member.role) content += ` - *Role: ${member.role}*`;
content += `\n`;
});
} else {
content += `1. **[Member Name]** ([Email]) - *Joined: [Date]* - *Role: [Optional Role]*\n`;
}
content += `\n`;
// Section 2: Membership
content += `## 2. Membership\n\n`;
content += `### Who Can Be a Member\n\nAny person who:\n\n`;
content += `${formData.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"}\n\n`;
content += `### Becoming a Member\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 += `2. Values alignment conversation\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 += `### Leaving the Cooperative\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 += `- Return their buy-in contribution within **${formData.buyInReturnDays || 90} days**\n`;
content += `- Maintain respectful ongoing relationships when possible\n\n`;
// Section 3: How We Make Decisions
content += `## 3. How We Make Decisions\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 += `Decisions under **$${formData.dayToDayLimit || 100}** can be made by any member. Just tell others what you did at the next meeting.\n\n`;
content += `### Regular Decisions\n\n`;
content += `Decisions between **$${formData.regularDecisionMin || 100}** and **$${formData.regularDecisionMax || 1000}** need consent from members present at a meeting (minimum 2 members).\n\n`;
content += `### Major Decisions\n\n`;
content += `These require consent from all members:\n\n`;
content += `- Adding or removing members\n`;
content += `- Changing this agreement\n`;
content += `- Taking on debt over **$${formData.majorDebtThreshold || 5000}**\n`;
content += `- Fundamental changes to our purpose or structure\n`;
content += `- Dissolution of ${cooperativeName}\n\n`;
content += `### Meeting Structure\n\n`;
content += `- Regular meetings happen **${formData.meetingFrequency || "weekly"}**\n`;
content += `- Emergency meetings need **${formData.emergencyNoticeHours || 24} hours** notice\n`;
content += `- We rotate who facilitates meetings\n`;
content += `- Decisions and reasoning get documented in shared notes\n\n`;
// Section 4: Money and Labour
content += `## 4. Money and Labour\n\n`;
content += `### Equal Ownership\n\n`;
content += `Each member owns an equal share of ${cooperativeName}, regardless of hours worked or tenure.\n\n`;
content += `### Paying Ourselves\n\n`;
const payPolicy = formData.payPolicy || "equal-pay";
const payPolicyName = payPolicy.replace('-', ' ').replace(/\b\w/g, (l: string) => l.toUpperCase());
content += `**Pay Policy:** ${data.payPolicyName || payPolicyName}\n\n`;
if (payPolicy === 'equal-pay') {
content += `All members receive equal compensation regardless of role or hours worked.\n\n`;
if (formData.baseRate) content += `- **Base rate:** $${formData.baseRate}/hour for all members\n`;
if (formData.monthlyDraw) content += `- **Equal monthly draw:** $${formData.monthlyDraw} per member\n`;
} else if (payPolicy === 'hours-weighted') {
content += `Compensation is proportional to hours worked by each member.\n\n`;
content += `- **Hourly rate:** $${formData.hourlyRate || 25}/hour\n`;
content += `- Members track their hours and are paid accordingly\n`;
content += `- Minimum hours commitment may apply\n`;
} else if (payPolicy === 'needs-weighted') {
content += `Compensation is allocated based on each member's individual financial needs.\n\n`;
content += `- Members declare their minimum monthly needs\n`;
content += `- Available payroll is distributed proportionally to cover needs\n`;
content += `- Regular needs assessment and adjustment process\n`;
content += `- **Minimum guaranteed amount:** $${formData.minGuaranteedPay || 1000}/month\n`;
}
content += `\n**Payment Schedule:**\n\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 += `### Work Expectations\n\n`;
content += `- Target hours per week: **${formData.targetHours || 40}** (flexible based on capacity)\n`;
content += `- We explicitly reject crunch culture\n`;
content += `- Members communicate their capacity openly\n`;
content += `- We adjust workload collectively when someone needs reduced hours\n\n`;
content += `### Financial Transparency\n\n`;
content += `- All members can access all financial records anytime\n`;
content += `- Monthly financial check-ins at meetings\n`;
content += `- Quarterly reviews of our runway (how many months we can operate)\n\n`;
// Section 5: Roles and Responsibilities
content += `## 5. Roles and Responsibilities\n\n`;
content += `### Rotating Roles\n\n`;
content += `We rotate operational roles every **${formData.roleRotationMonths || 6} months**. Current roles include:\n\n`;
content += `${formData.rotatingRoles || "Financial coordinator (handles bookkeeping, not financial decisions) \nMeeting facilitator \nExternal communications \nOthers"}\n\n`;
content += `### Shared Responsibilities\n\n`;
content += `All members participate in:\n\n`;
content += `${formData.sharedResponsibilities || "Governance and decision-making \nStrategic planning \nMutual support and care"}\n\n`;
// Section 6: Conflict and Care
content += `## 6. Conflict and Care\n\n`;
content += `### When Conflict Happens\n\n`;
content += `1. Direct conversation between parties (if comfortable)\n`;
content += `2. Mediation with another member\n`;
content += `3. Full group discussion with structured process\n`;
content += `4. External mediation if needed\n\n`;
content += `### Care Commitments\n\n`;
content += `- We check in about capacity and wellbeing regularly\n`;
content += `- We honour diverse access needs\n`;
content += `- We maintain flexibility for life circumstances\n`;
content += `- We contribute to mutual aid when members face hardship\n\n`;
// Section 7: Changing This Agreement
content += `## 7. Changing This Agreement\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
content += `## 8. If We Need to Close\n\n`;
content += `If ${cooperativeName} dissolves:\n\n`;
content += `1. Pay all debts and obligations\n`;
content += `2. Return member contributions\n`;
content += `3. Distribute remaining assets equally among members\n`;
if (formData.assetDonationTarget) {
content += `4. Or donate remaining assets to **${formData.assetDonationTarget}**\n`;
}
content += `\n`;
// Section 9: Legal Registration
content += `## 9. Legal Registration\n\n`;
if (formData.isLegallyRegistered) {
content += `- **Legal Structure:** ${formData.legalStructure || "[Cooperative corporation, LLC, partnership, etc.]"}\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;
};
// Conflict Resolution Framework formatting - Complete document with all static and dynamic content
const formatConflictResolutionAsText = (data: any): string => {
const formData = data.formData || {};
let content = `CONFLICT RESOLUTION FRAMEWORK\n=============================\n\n`;
// Section 1: Organization Information
content += `1. ORGANIZATION INFORMATION\n---------------------------\n\n`;
content += `Organization Name: ${formData.orgName || '[Enter your organization name]'}\n`;
content += `Organization Type: ${formData.orgType || '[Select organization type]'}\n`;
content += `Number of Members/Staff: ${formData.memberCount || '[e.g., 5]'}\n\n`;
// Section 2: Guiding Principles & Values
if (data.sectionsEnabled?.values !== false) {
content += `2. GUIDING PRINCIPLES & VALUES\n------------------------------\n\n`;
content += `Our Core Values:\n`;
if (formData.coreValues && Array.isArray(formData.coreValues) && formData.coreValues.length > 0) {
formData.coreValues.forEach((value: string, index: number) => {
content += `${index + 1}. ${value || '[Value not specified]'}\n`;
});
} else {
content += `[List your organization's core values - these guide how you approach conflict]\n`;
}
content += `\n`;
content += `Conflict Resolution Principles:\n`;
if (formData.principles && Array.isArray(formData.principles) && formData.principles.length > 0) {
formData.principles.forEach((principle: string, index: number) => {
content += `${index + 1}. ${principle || '[Principle not specified]'}\n`;
});
} else {
content += `[List principles that will guide how conflicts are resolved in your organization]\n`;
}
content += `\n`;
}
// Section 3: Prevention & Early Intervention
if (data.sectionsEnabled?.prevention !== false) {
content += `3. PREVENTION & EARLY INTERVENTION\n----------------------------------\n\n`;
content += `Member Involvement in Conflict Resolution:\n`;
content += `${formData.memberInvolvement || '[Describe how members participate in conflict resolution processes]'}\n\n`;
content += `Communication Guidelines:\n`;
content += `${formData.communicationGuidelines || '[Outline communication standards and expectations for healthy dialogue]'}\n\n`;
}
// Section 4: Reporting & Documentation
if (data.sectionsEnabled?.reporting !== false) {
content += `4. REPORTING & DOCUMENTATION\n----------------------------\n\n`;
content += `How to Report Conflicts:\n`;
content += `Conflicts can be reported to:\n`;
// Add report receivers based on form data
if (data.reportReceivers && Array.isArray(data.reportReceivers)) {
data.reportReceivers.forEach((receiver: any) => {
if (receiver.checked) {
content += `${receiver.label}\n`;
}
});
}
content += `\n`;
content += `Mediator/Facilitator Structure: ${formData.mediatorType || '[Select mediator structure]'}\n`;
if (formData.supportPeople) {
content += `Support people are allowed in mediation sessions for emotional support.\n`;
}
content += `\n`;
}
// Section 5: Process Steps
if (data.sectionsEnabled?.process !== false) {
content += `5. PROCESS STEPS\n----------------\n\n`;
content += `Process Steps:\n`;
content += `${formData.processSteps || '[Describe the step-by-step conflict resolution process]'}\n\n`;
content += `Escalation Criteria:\n`;
content += `${formData.escalationCriteria || '[Define when and how conflicts escalate to higher levels]'}\n\n`;
content += `Mediation Process:\n`;
content += `${formData.mediation || '[Describe the mediation process and procedures]'}\n\n`;
content += `Final Decision Making:\n`;
content += `${formData.finalDecision || '[Explain how final decisions are made when mediation does not resolve the conflict]'}\n\n`;
}
// Section 6: Timeline & Process
if (data.sectionsEnabled?.timeline !== false) {
content += `6. TIMELINE & PROCESS\n--------------------\n\n`;
content += `Initial Response Time: ${formData.initialResponse || '[Select response time]'}\n`;
content += `Target Resolution Time: ${formData.resolutionTarget || '[Select target time]'}\n\n`;
}
// Section 7: Learning & Follow-up
if (data.sectionsEnabled?.learning !== false) {
content += `7. LEARNING & FOLLOW-UP\n-----------------------\n\n`;
content += `Learning and Improvement:\n`;
content += `${formData.learning || '[Describe how the organization learns from conflicts and improves processes]'}\n\n`;
}
// Section 8: Emergency Procedures
if (data.sectionsEnabled?.emergency !== false) {
content += `8. EMERGENCY PROCEDURES\n-----------------------\n\n`;
content += `Emergency Procedures:\n`;
content += `${formData.emergencyProcedures || '[Outline procedures for urgent or severe conflicts requiring immediate action]'}\n\n`;
}
// Section 9: Review & Updates
if (data.sectionsEnabled?.review !== false) {
content += `9. REVIEW & UPDATES\n-------------------\n\n`;
content += `Annual Review Process:\n`;
content += `${formData.annualReview || '[Describe how this framework will be reviewed and updated annually]'}\n\n`;
}
return content;
};
const formatConflictResolutionAsMarkdown = (data: any): string => {
const formData = data.formData || {};
let content = `# Conflict Resolution Framework\n\n`;
// Section 1: Organization Information
content += `## 1. Organization Information\n\n`;
content += `**Organization Name:** ${formData.orgName || '[Enter your organization name]'} \n`;
content += `**Organization Type:** ${formData.orgType || '[Select organization type]'} \n`;
content += `**Number of Members/Staff:** ${formData.memberCount || '[e.g., 5]'} \n\n`;
// Section 2: Guiding Principles & Values
if (data.sectionsEnabled?.values !== false) {
content += `## 2. Guiding Principles & Values\n\n`;
content += `### Our Core Values\n\n`;
if (formData.coreValues && Array.isArray(formData.coreValues) && formData.coreValues.length > 0) {
formData.coreValues.forEach((value: string, index: number) => {
content += `${index + 1}. ${value || '[Value not specified]'}\n`;
});
} else {
content += `*[List your organization's core values - these guide how you approach conflict]*\n`;
}
content += `\n`;
content += `### Conflict Resolution Principles\n\n`;
if (formData.principles && Array.isArray(formData.principles) && formData.principles.length > 0) {
formData.principles.forEach((principle: string, index: number) => {
content += `${index + 1}. ${principle || '[Principle not specified]'}\n`;
});
} else {
content += `*[List principles that will guide how conflicts are resolved in your organization]*\n`;
}
content += `\n`;
}
// Section 3: Prevention & Early Intervention
if (data.sectionsEnabled?.prevention !== false) {
content += `## 3. Prevention & Early Intervention\n\n`;
content += `### Member Involvement in Conflict Resolution\n\n`;
content += `${formData.memberInvolvement || '*[Describe how members participate in conflict resolution processes]*'}\n\n`;
content += `### Communication Guidelines\n\n`;
content += `${formData.communicationGuidelines || '*[Outline communication standards and expectations for healthy dialogue]*'}\n\n`;
}
// Section 4: Reporting & Documentation
if (data.sectionsEnabled?.reporting !== false) {
content += `## 4. Reporting & Documentation\n\n`;
content += `### How to Report Conflicts\n\n`;
content += `Conflicts can be reported to:\n\n`;
// Add report receivers based on form data
if (data.reportReceivers && Array.isArray(data.reportReceivers)) {
const checkedReceivers = data.reportReceivers.filter((receiver: any) => receiver.checked);
if (checkedReceivers.length > 0) {
checkedReceivers.forEach((receiver: any) => {
content += `- ${receiver.label}\n`;
});
} else {
content += `*[Select who can receive conflict reports]*\n`;
}
} else {
content += `*[Select who can receive conflict reports]*\n`;
}
content += `\n`;
content += `**Mediator/Facilitator Structure:** ${formData.mediatorType || '[Select mediator structure]'}\n\n`;
if (formData.supportPeople) {
content += `**Support People:** Allowed in mediation sessions for emotional support\n\n`;
}
}
// Section 5: Process Steps
if (data.sectionsEnabled?.process !== false) {
content += `## 5. Process Steps\n\n`;
content += `### Process Steps\n\n`;
content += `${formData.processSteps || '*[Describe the step-by-step conflict resolution process]*'}\n\n`;
content += `### Escalation Criteria\n\n`;
content += `${formData.escalationCriteria || '*[Define when and how conflicts escalate to higher levels]*'}\n\n`;
content += `### Mediation Process\n\n`;
content += `${formData.mediation || '*[Describe the mediation process and procedures]*'}\n\n`;
content += `### Final Decision Making\n\n`;
content += `${formData.finalDecision || '*[Explain how final decisions are made when mediation does not resolve the conflict]*'}\n\n`;
}
// Section 6: Timeline & Process
if (data.sectionsEnabled?.timeline !== false) {
content += `## 6. Timeline & Process\n\n`;
content += `- **Initial Response Time:** ${formData.initialResponse || '[Select response time]'}\n`;
content += `- **Target Resolution Time:** ${formData.resolutionTarget || '[Select target time]'}\n\n`;
}
// Section 7: Learning & Follow-up
if (data.sectionsEnabled?.learning !== false) {
content += `## 7. Learning & Follow-up\n\n`;
content += `### Learning and Improvement\n\n`;
content += `${formData.learning || '*[Describe how the organization learns from conflicts and improves processes]*'}\n\n`;
}
// Section 8: Emergency Procedures
if (data.sectionsEnabled?.emergency !== false) {
content += `## 8. Emergency Procedures\n\n`;
content += `${formData.emergencyProcedures || '*[Outline procedures for urgent or severe conflicts requiring immediate action]*'}\n\n`;
}
// Section 9: Review & Updates
if (data.sectionsEnabled?.review !== false) {
content += `## 9. Review & Updates\n\n`;
content += `### Annual Review Process\n\n`;
content += `${formData.annualReview || '*[Describe how this framework will be reviewed and updated annually]*'}\n\n`;
}
return content;
};
// Decision Framework formatting
const formatDecisionFrameworkAsText = (data: any): string => {
let content = `DECISION FRAMEWORK RESULTS\n=========================\n\n`;
if (data.surveyResponses && typeof data.surveyResponses === 'object') {
content += `SURVEY RESPONSES\n----------------\n`;
content += `Urgency: ${data.surveyResponses.urgency || '[Not answered]'}\n`;
content += `Reversible: ${data.surveyResponses.reversible || '[Not answered]'}\n`;
content += `Expertise: ${data.surveyResponses.expertise || '[Not answered]'}\n`;
content += `Impact: ${data.surveyResponses.impact || '[Not answered]'}\n`;
content += `Options: ${data.surveyResponses.options || '[Not answered]'}\n`;
content += `Investment: ${data.surveyResponses.investment || '[Not answered]'}\n`;
content += `Team Size: ${data.surveyResponses.teamSize || '[Not answered]'}\n\n`;
}
if (data.recommendedFramework && typeof data.recommendedFramework === 'object') {
const framework = data.recommendedFramework;
content += `RECOMMENDED FRAMEWORK\n--------------------\n`;
content += `Method: ${framework.method || '[No method specified]'}\n`;
content += `Tagline: ${framework.tagline || '[No tagline]'}\n\n`;
content += `Reasoning: ${framework.reasoning || '[No reasoning provided]'}\n\n`;
if (framework.steps && Array.isArray(framework.steps) && framework.steps.length > 0) {
content += `IMPLEMENTATION STEPS\n-------------------\n`;
framework.steps.forEach((step: string, index: number) => {
content += `${index + 1}. ${step || '[Step not specified]'}\n`;
});
content += "\n";
}
if (framework.tips && Array.isArray(framework.tips) && framework.tips.length > 0) {
content += `PRO TIPS\n--------\n`;
framework.tips.forEach((tip: string, index: number) => {
content += `${index + 1}. ${tip || '[Tip not specified]'}\n`;
});
content += "\n";
}
}
return content;
};
const formatDecisionFrameworkAsMarkdown = (data: any): string => {
let content = `## Decision Framework Results\n\n`;
if (data.surveyResponses && typeof data.surveyResponses === 'object') {
content += `### Survey Responses\n\n`;
content += `**Urgency:** ${data.surveyResponses.urgency || '[Not answered]'} \n`;
content += `**Reversible:** ${data.surveyResponses.reversible || '[Not answered]'} \n`;
content += `**Expertise:** ${data.surveyResponses.expertise || '[Not answered]'} \n`;
content += `**Impact:** ${data.surveyResponses.impact || '[Not answered]'} \n`;
content += `**Options:** ${data.surveyResponses.options || '[Not answered]'} \n`;
content += `**Investment:** ${data.surveyResponses.investment || '[Not answered]'} \n`;
content += `**Team Size:** ${data.surveyResponses.teamSize || '[Not answered]'} \n\n`;
}
if (data.recommendedFramework && typeof data.recommendedFramework === 'object') {
const framework = data.recommendedFramework;
content += `### Recommended Framework\n\n`;
content += `**Method:** ${framework.method || '[No method specified]'} \n`;
content += `**Tagline:** ${framework.tagline || '[No tagline]'} \n\n`;
content += `**Reasoning:** ${framework.reasoning || '[No reasoning provided]'}\n\n`;
if (framework.steps && Array.isArray(framework.steps) && framework.steps.length > 0) {
content += `#### Implementation Steps\n\n`;
framework.steps.forEach((step: string, index: number) => {
content += `${index + 1}. ${step || '[Step not specified]'}\n`;
});
content += "\n";
}
if (framework.tips && Array.isArray(framework.tips) && framework.tips.length > 0) {
content += `#### Pro Tips\n\n`;
framework.tips.forEach((tip: string, index: number) => {
content += `${index + 1}. ${tip || '[Tip not specified]'}\n`;
});
content += "\n";
}
}
return content;
};
const downloadFile = (content: string, filename: string, type: string) => {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
</script>
<style scoped>
@reference "tailwindcss";
.export-content {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
padding: 1.5rem;
gap: 1rem;
}
.export-section {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
}
.export-buttons {
@apply font-mono;
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.export-btn {
@apply bg-neutral-100 dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 text-neutral-900 dark:text-white;
}
.export-btn:hover:not(:disabled) {
background: #000;
color: #fff;
transform: translateY(-2px) translateX(-2px);
box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3);
}
.export-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.export-btn.success-state {
background: #10b981 !important;
color: white !important;
border-color: #10b981 !important;
}
.success-icon {
position: absolute;
top: 50%;
right: 0.5rem;
transform: translateY(-50%);
animation: successPulse 2s ease-out;
color: #10b981;
}
.export-btn.success-state .success-icon {
color: white;
}
@keyframes successPulse {
0% {
opacity: 0;
transform: translateY(-50%) scale(0.5);
}
20% {
opacity: 1;
transform: translateY(-50%) scale(1.2);
}
40% {
transform: translateY(-50%) scale(1);
}
100% {
opacity: 0;
transform: translateY(-50%) scale(1);
}
}
/* Mobile responsive */
@media (max-width: 768px) {
.export-content {
flex-direction: column;
align-items: stretch;
}
.export-section {
justify-content: center;
}
.export-buttons {
justify-content: center;
}
.export-btn {
flex: 1;
justify-content: center;
min-width: 140px;
}
}
</style>