diff --git a/app.vue b/app.vue index 32ef7bc..e8aa8ad 100644 --- a/app.vue +++ b/app.vue @@ -91,7 +91,7 @@ const isCoopBuilderSection = computed( route.path === "/dashboard" || route.path === "/mix" || route.path === "/budget" || - route.path === "/runway-lite" || + route.path === "/cash-flow" || route.path === "/settings" || route.path === "/glossary" ); diff --git a/components/AnnualBudget.vue b/components/AnnualBudget.vue index 8d04e2e..0e8c844 100644 --- a/components/AnnualBudget.vue +++ b/components/AnnualBudget.vue @@ -3,13 +3,18 @@

Annual Budget Overview

- -
+ +
- - + + @@ -18,24 +23,25 @@ - + - - + + - + - + @@ -47,14 +53,15 @@ - - - - - - - + + - + - + @@ -96,8 +99,10 @@ - - + + @@ -107,7 +112,6 @@
CategoryPlanned + Category + + Planned + %
REVENUE
{{ category.name }}
+ {{ category.name }} + {{ formatCurrency(category.planned) }} - {{ category.percentage }}% - {{ category.percentage }}%
Total RevenueTotal Revenue {{ formatCurrency(totalRevenuePlanned) }}

{{ diversificationGuidance }}

-

- Consider developing: {{ suggestedCategories.join(', ') }} +

+ Consider developing: {{ suggestedCategories.join(", ") }}

- + Learn how to develop these revenue streams →

@@ -62,33 +69,29 @@
EXPENSES
{{ category.name }}
+ {{ category.name }} + {{ formatCurrency(category.planned) }} - {{ category.percentage }}% - {{ category.percentage }}%
Total ExpensesTotal Expenses {{ formatCurrency(totalExpensesPlanned) }}
NET TOTAL
NET TOTAL {{ formatCurrency(netTotal) }}
-
@@ -118,7 +122,7 @@ interface Props { } const props = withDefaults(defineProps(), { - year: () => new Date().getFullYear() + year: () => new Date().getFullYear(), }); // Get budget data from store @@ -127,19 +131,54 @@ const budgetStore = useBudgetStore(); // Revenue categories with calculations const revenueCategories = computed(() => { const categories = [ - { key: 'gamesProducts', name: 'Games & Products', planned: 0, percentage: 0 }, - { key: 'servicesContracts', name: 'Services & Contracts', planned: 0, percentage: 0 }, - { key: 'grantsFunding', name: 'Grants & Funding', planned: 0, percentage: 0 }, - { key: 'communitySupport', name: 'Community Support', planned: 0, percentage: 0 }, - { key: 'partnerships', name: 'Partnerships', planned: 0, percentage: 0 }, - { key: 'investmentIncome', name: 'Investment Income', planned: 0, percentage: 0 }, - { key: 'inKindContributions', name: 'In-Kind Contributions', planned: 0, percentage: 0 }, + { + key: "gamesProducts", + name: "Games & Products", + planned: 0, + percentage: 0, + }, + { + key: "servicesContracts", + name: "Services & Contracts", + planned: 0, + percentage: 0, + }, + { + key: "grantsFunding", + name: "Grants & Funding", + planned: 0, + percentage: 0, + }, + { + key: "communitySupport", + name: "Community Support", + planned: 0, + percentage: 0, + }, + { key: "partnerships", name: "Partnerships", planned: 0, percentage: 0 }, + { + key: "investmentIncome", + name: "Investment Income", + planned: 0, + percentage: 0, + }, + { + key: "inKindContributions", + name: "In-Kind Contributions", + planned: 0, + percentage: 0, + }, ]; // Calculate planned amounts for each category - budgetStore.budgetWorksheet.revenue.forEach(item => { - const annualPlanned = Object.values(item.monthlyValues || {}).reduce((sum, val) => sum + (val || 0), 0); - const categoryIndex = categories.findIndex(cat => cat.name === item.mainCategory); + budgetStore.budgetWorksheet.revenue.forEach((item) => { + const annualPlanned = Object.values(item.monthlyValues || {}).reduce( + (sum, val) => sum + (val || 0), + 0 + ); + const categoryIndex = categories.findIndex( + (cat) => cat.name === item.mainCategory + ); if (categoryIndex !== -1) { categories[categoryIndex].planned += annualPlanned; } @@ -147,30 +186,34 @@ const revenueCategories = computed(() => { // Calculate percentages const total = categories.reduce((sum, cat) => sum + cat.planned, 0); - categories.forEach(cat => { + categories.forEach((cat) => { cat.percentage = total > 0 ? Math.round((cat.planned / total) * 100) : 0; }); return categories; }); - // Expense categories with calculations const expenseCategories = computed(() => { const categories = [ - { name: 'Salaries & Benefits', planned: 0, percentage: 0 }, - { name: 'Development Costs', planned: 0, percentage: 0 }, - { name: 'Equipment & Technology', planned: 0, percentage: 0 }, - { name: 'Marketing & Outreach', planned: 0, percentage: 0 }, - { name: 'Office & Operations', planned: 0, percentage: 0 }, - { name: 'Legal & Professional', planned: 0, percentage: 0 }, - { name: 'Other Expenses', planned: 0, percentage: 0 }, + { name: "Salaries & Benefits", planned: 0, percentage: 0 }, + { name: "Development Costs", planned: 0, percentage: 0 }, + { name: "Equipment & Technology", planned: 0, percentage: 0 }, + { name: "Marketing & Outreach", planned: 0, percentage: 0 }, + { name: "Office & Operations", planned: 0, percentage: 0 }, + { name: "Legal & Professional", planned: 0, percentage: 0 }, + { name: "Other Expenses", planned: 0, percentage: 0 }, ]; // Calculate planned amounts for each category - budgetStore.budgetWorksheet.expenses.forEach(item => { - const annualPlanned = Object.values(item.monthlyValues || {}).reduce((sum, val) => sum + (val || 0), 0); - const categoryIndex = categories.findIndex(cat => cat.name === item.mainCategory); + budgetStore.budgetWorksheet.expenses.forEach((item) => { + const annualPlanned = Object.values(item.monthlyValues || {}).reduce( + (sum, val) => sum + (val || 0), + 0 + ); + const categoryIndex = categories.findIndex( + (cat) => cat.name === item.mainCategory + ); if (categoryIndex !== -1) { categories[categoryIndex].planned += annualPlanned; } @@ -178,7 +221,7 @@ const expenseCategories = computed(() => { // Calculate percentages const total = categories.reduce((sum, cat) => sum + cat.planned, 0); - categories.forEach(cat => { + categories.forEach((cat) => { cat.percentage = total > 0 ? Math.round((cat.planned / total) * 100) : 0; }); @@ -186,33 +229,39 @@ const expenseCategories = computed(() => { }); // Totals -const totalRevenuePlanned = computed(() => +const totalRevenuePlanned = computed(() => revenueCategories.value.reduce((sum, cat) => sum + cat.planned, 0) ); -const totalExpensesPlanned = computed(() => +const totalExpensesPlanned = computed(() => expenseCategories.value.reduce((sum, cat) => sum + cat.planned, 0) ); -const netTotal = computed(() => - totalRevenuePlanned.value - totalExpensesPlanned.value +const netTotal = computed( + () => totalRevenuePlanned.value - totalExpensesPlanned.value ); const netTotalClass = computed(() => { - if (netTotal.value > 0) return 'bg-green-50'; - if (netTotal.value < 0) return 'bg-red-50'; - return 'bg-gray-50'; + if (netTotal.value > 0) return "bg-green-50"; + if (netTotal.value < 0) return "bg-red-50"; + return "bg-gray-50"; }); - // Diversification guidance const diversificationGuidance = computed(() => { - const categoriesWithRevenue = revenueCategories.value.filter(cat => cat.percentage > 0); - const topCategory = categoriesWithRevenue.reduce((max, cat) => cat.percentage > max.percentage ? cat : max, { percentage: 0, name: '' }); - const categoriesAbove20 = categoriesWithRevenue.filter(cat => cat.percentage >= 20).length; - + const categoriesWithRevenue = revenueCategories.value.filter( + (cat) => cat.percentage > 0 + ); + const topCategory = categoriesWithRevenue.reduce( + (max, cat) => (cat.percentage > max.percentage ? cat : max), + { percentage: 0, name: "" } + ); + const categoriesAbove20 = categoriesWithRevenue.filter( + (cat) => cat.percentage >= 20 + ).length; + let guidance = ""; - + // Concentration Risk if (topCategory.percentage >= 70) { guidance += `Very high concentration risk: most of your revenue is from ${topCategory.name} (${topCategory.percentage}%). `; @@ -221,107 +270,141 @@ const diversificationGuidance = computed(() => { } else { guidance += "No single category dominates your revenue. "; } - + // Diversification Benchmark if (categoriesAbove20 >= 3) { guidance += "Your mix is reasonably balanced across multiple sources."; } else if (categoriesAbove20 === 2) { guidance += "Your mix is split, but still reliant on just two sources."; } else { - guidance += "Your revenue is concentrated; aim to grow at least 2–3 other categories."; + guidance += + "Your revenue is concentrated; aim to grow at least 2–3 other categories."; } - + // Optional Positive Nudges - const grantsCategory = categoriesWithRevenue.find(cat => cat.name === 'Grants & Funding'); - const servicesCategory = categoriesWithRevenue.find(cat => cat.name === 'Services & Contracts'); - const productsCategory = categoriesWithRevenue.find(cat => cat.name === 'Games & Products'); - + const grantsCategory = categoriesWithRevenue.find( + (cat) => cat.name === "Grants & Funding" + ); + const servicesCategory = categoriesWithRevenue.find( + (cat) => cat.name === "Services & Contracts" + ); + const productsCategory = categoriesWithRevenue.find( + (cat) => cat.name === "Games & Products" + ); + if (grantsCategory && grantsCategory.percentage >= 20) { - guidance += " You've secured meaningful support from grants — consider pairing this with services or product revenue for stability."; - } else if (servicesCategory && servicesCategory.percentage >= 20 && productsCategory && productsCategory.percentage >= 20) { - guidance += " Strong foundation in both services and products — this balance helps smooth revenue timing."; + guidance += + " You've secured meaningful support from grants — consider pairing this with services or product revenue for stability."; + } else if ( + servicesCategory && + servicesCategory.percentage >= 20 && + productsCategory && + productsCategory.percentage >= 20 + ) { + guidance += + " Strong foundation in both services and products — this balance helps smooth revenue timing."; } - + return guidance; }); const guidanceBackgroundClass = computed(() => { - const topCategory = revenueCategories.value.reduce((max, cat) => cat.percentage > max.percentage ? cat : max, { percentage: 0 }); - + const topCategory = revenueCategories.value.reduce( + (max, cat) => (cat.percentage > max.percentage ? cat : max), + { percentage: 0 } + ); + if (topCategory.percentage >= 70) { - return 'bg-red-50'; + return "bg-red-50"; } else if (topCategory.percentage >= 50) { - return 'bg-red-50'; + return "bg-red-50"; } else { - const categoriesAbove20 = revenueCategories.value.filter(cat => cat.percentage >= 20).length; + const categoriesAbove20 = revenueCategories.value.filter( + (cat) => cat.percentage >= 20 + ).length; if (categoriesAbove20 >= 3) { - return 'bg-green-50'; + return "bg-green-50"; } else { - return 'bg-yellow-50'; + return "bg-yellow-50"; } } }); // Suggested categories to develop const suggestedCategories = computed(() => { - const categoriesWithRevenue = revenueCategories.value.filter(cat => cat.percentage > 0); - const categoriesWithoutRevenue = revenueCategories.value.filter(cat => cat.percentage === 0); - const categoriesAbove20 = categoriesWithRevenue.filter(cat => cat.percentage >= 20).length; - + const categoriesWithRevenue = revenueCategories.value.filter( + (cat) => cat.percentage > 0 + ); + const categoriesWithoutRevenue = revenueCategories.value.filter( + (cat) => cat.percentage === 0 + ); + const categoriesAbove20 = categoriesWithRevenue.filter( + (cat) => cat.percentage >= 20 + ).length; + // If we have fewer than 3 categories above 20%, suggest developing others if (categoriesAbove20 < 3) { // Prioritize categories that complement existing strengths const suggestions = []; - + // If they have services, suggest products for balance - if (categoriesWithRevenue.some(cat => cat.name === 'Services & Contracts') && - !categoriesWithRevenue.some(cat => cat.name === 'Games & Products')) { - suggestions.push('Games & Products'); + if ( + categoriesWithRevenue.some( + (cat) => cat.name === "Services & Contracts" + ) && + !categoriesWithRevenue.some((cat) => cat.name === "Games & Products") + ) { + suggestions.push("Games & Products"); } - + // If they have products, suggest services for stability - if (categoriesWithRevenue.some(cat => cat.name === 'Games & Products') && - !categoriesWithRevenue.some(cat => cat.name === 'Services & Contracts')) { - suggestions.push('Services & Contracts'); + if ( + categoriesWithRevenue.some((cat) => cat.name === "Games & Products") && + !categoriesWithRevenue.some((cat) => cat.name === "Services & Contracts") + ) { + suggestions.push("Services & Contracts"); } - + // Always suggest grants if not present - if (!categoriesWithRevenue.some(cat => cat.name === 'Grants & Funding')) { - suggestions.push('Grants & Funding'); + if (!categoriesWithRevenue.some((cat) => cat.name === "Grants & Funding")) { + suggestions.push("Grants & Funding"); } - + // Add community support for stability - if (!categoriesWithRevenue.some(cat => cat.name === 'Community Support')) { - suggestions.push('Community Support'); + if ( + !categoriesWithRevenue.some((cat) => cat.name === "Community Support") + ) { + suggestions.push("Community Support"); } - + return suggestions.slice(0, 3); // Limit to 3 suggestions } - + return []; }); // Utility functions function formatCurrency(amount: number): string { - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(amount || 0); } function getPercentageClass(percentage: number): string { - if (percentage > 50) return 'text-red-600 font-bold'; - if (percentage > 35) return 'text-yellow-600 font-semibold'; - if (percentage > 20) return 'text-black font-medium'; - return 'text-gray-500'; + if (percentage > 50) return "text-red-600 font-bold"; + if (percentage > 35) return "text-yellow-600 font-semibold"; + if (percentage > 20) return "text-black font-medium"; + return "text-gray-500"; } - // Initialize onMounted(() => { - console.log(`Annual budget view for org: ${props.orgId}, year: ${props.year}`); + console.log( + `Annual budget view for org: ${props.orgId}, year: ${props.year}` + ); }); @@ -336,4 +419,4 @@ input[type="number"]::-webkit-outer-spin-button { input[type="number"] { -moz-appearance: textfield; } - \ No newline at end of file + diff --git a/components/CashFlowChart.vue b/components/CashFlowChart.vue new file mode 100644 index 0000000..e50f452 --- /dev/null +++ b/components/CashFlowChart.vue @@ -0,0 +1,295 @@ + + + \ No newline at end of file diff --git a/components/CoopBuilderSubnav.vue b/components/CoopBuilderSubnav.vue index f0e875a..c44f5c1 100644 --- a/components/CoopBuilderSubnav.vue +++ b/components/CoopBuilderSubnav.vue @@ -26,30 +26,25 @@ const route = useRoute(); const coopBuilderItems = [ - { - id: "dashboard", - name: "Dashboard", - path: "/dashboard", - }, { id: "coop-builder", name: "Setup Wizard", path: "/coop-builder", }, + { + id: "dashboard", + name: "Compensation", + path: "/dashboard", + }, { id: "budget", name: "Budget", path: "/budget", }, { - id: "mix", - name: "Revenue Mix", - path: "/mix", - }, - { - id: "runway-lite", - name: "Runway Lite", - path: "/runway-lite", + id: "cash-flow", + name: "Cash Flow", + path: "/cash-flow", }, ]; diff --git a/components/ExportOptions.vue b/components/ExportOptions.vue index abd25a6..e5a7ef6 100644 --- a/components/ExportOptions.vue +++ b/components/ExportOptions.vue @@ -60,18 +60,45 @@ const showSuccessFeedback = ( buttonRef: Ref, successRef: Ref ) => { - if (!buttonRef.value) return; + try { + successRef.value = true; - 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); + } - // Add checkmark overlay animation - const button = buttonRef.value; - button.classList.add("success-state"); - - setTimeout(() => { - successRef.value = false; - button.classList.remove("success-state"); - }, 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 () => { @@ -82,30 +109,64 @@ const copyToClipboard = async () => { try { const textContent = extractTextContent(); - if (navigator.clipboard && navigator.clipboard.writeText) { - await navigator.clipboard.writeText(textContent); - } else { - // Fallback for browsers without clipboard API - const textarea = document.createElement("textarea"); - textarea.value = textContent; - textarea.style.position = "fixed"; - textarea.style.opacity = "0"; - document.body.appendChild(textarea); - textarea.focus(); - textarea.select(); - - const successful = document.execCommand("copy"); - document.body.removeChild(textarea); - - if (!successful) { - throw new Error("execCommand copy failed"); + // 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); } } - showSuccessFeedback(copyButton, showCopySuccess); } catch (error) { console.error("Copy failed:", error); - alert("Copy failed. Please try again or use the download option."); + alert("Copy failed. Please try the Download Markdown button instead."); } finally { isProcessing.value = false; } @@ -209,18 +270,24 @@ const formatDataAsMarkdown = (data: any): string => { }; const formatTechCharterAsText = (data: any): string => { - let content = `PURPOSE\n-------\n\n${data.charterPurpose}\n\n`; + let content = `PURPOSE\n-------\n\n${data.charterPurpose || '[No purpose defined]'}\n\n`; - const selectedPrinciples = Object.keys(data.principleWeights).filter( - (p) => data.principleWeights[p] > 0 + 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) => !data.nonNegotiables.includes(p)).length > 0) { + if (selectedPrinciples.filter((p) => !nonNegotiables.includes(p)).length > 0) { content += `CORE PRINCIPLES\n---------------\n\n`; selectedPrinciples - .filter((p) => !data.nonNegotiables.includes(p)) + .filter((p) => !nonNegotiables.includes(p)) .forEach((pId) => { - const principle = data.principles.find((p: any) => p.id === pId); + const principle = principles.find((p: any) => p.id === pId); if (principle) { content += `• ${principle.name}\n`; } @@ -228,10 +295,10 @@ const formatTechCharterAsText = (data: any): string => { content += "\n"; } - if (data.nonNegotiables.length > 0) { + if (nonNegotiables.length > 0) { content += `NON-NEGOTIABLE REQUIREMENTS\n---------------------------\n\nAny vendor failing these requirements is automatically disqualified.\n\n`; - data.nonNegotiables.forEach((pId: string) => { - const principle = data.principles.find((p: any) => p.id === pId); + nonNegotiables.forEach((pId: string) => { + const principle = principles.find((p: any) => p.id === pId); if (principle) { content += `• ${principle.name}\n`; } @@ -240,19 +307,19 @@ const formatTechCharterAsText = (data: any): string => { } content += `TECHNICAL CONSTRAINTS\n---------------------\n\n`; - content += `• Authentication: ${data.constraints.sso}\n`; - content += `• Hosting: ${data.constraints.hosting}\n`; - if (data.constraints.integrations.length > 0) { - content += `• Required Integrations: ${data.constraints.integrations.join(", ")}\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: ${data.constraints.support}\n`; - content += `• Migration Timeline: ${data.constraints.timeline}\n\n`; + content += `• Support Level: ${constraints.support || 'Not specified'}\n`; + content += `• Migration Timeline: ${constraints.timeline || 'Not specified'}\n\n`; - if (data.sortedWeights.length > 0) { + if (sortedWeights.length > 0) { content += `EVALUATION RUBRIC\n-----------------\n\nScore each vendor option using these weighted criteria (0-5 scale):\n\n`; - data.sortedWeights.forEach((principle: any) => { - content += `${principle.name} (Weight: ${data.principleWeights[principle.id]})\n${ - principle.rubricDescription + sortedWeights.forEach((principle: any) => { + content += `${principle.name} (Weight: ${principleWeights[principle.id] || 0})\n${ + principle.rubricDescription || 'No description provided' }\n\n`; }); } @@ -261,18 +328,24 @@ const formatTechCharterAsText = (data: any): string => { }; const formatTechCharterAsMarkdown = (data: any): string => { - let content = `## Purpose\n\n${data.charterPurpose}\n\n`; + let content = `## Purpose\n\n${data.charterPurpose || '[No purpose defined]'}\n\n`; - const selectedPrinciples = Object.keys(data.principleWeights).filter( - (p) => data.principleWeights[p] > 0 + 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) => !data.nonNegotiables.includes(p)).length > 0) { + if (selectedPrinciples.filter((p) => !nonNegotiables.includes(p)).length > 0) { content += `## Core Principles\n\n`; selectedPrinciples - .filter((p) => !data.nonNegotiables.includes(p)) + .filter((p) => !nonNegotiables.includes(p)) .forEach((pId) => { - const principle = data.principles.find((p: any) => p.id === pId); + const principle = principles.find((p: any) => p.id === pId); if (principle) { content += `- ${principle.name}\n`; } @@ -280,10 +353,10 @@ const formatTechCharterAsMarkdown = (data: any): string => { content += "\n"; } - if (data.nonNegotiables.length > 0) { + if (nonNegotiables.length > 0) { content += `## Non-Negotiable Requirements\n\n**Any vendor failing these requirements is automatically disqualified.**\n\n`; - data.nonNegotiables.forEach((pId: string) => { - const principle = data.principles.find((p: any) => p.id === pId); + nonNegotiables.forEach((pId: string) => { + const principle = principles.find((p: any) => p.id === pId); if (principle) { content += `- **${principle.name}**\n`; } @@ -292,21 +365,21 @@ const formatTechCharterAsMarkdown = (data: any): string => { } content += `## Technical Constraints\n\n`; - content += `- Authentication: ${data.constraints.sso}\n`; - content += `- Hosting: ${data.constraints.hosting}\n`; - if (data.constraints.integrations.length > 0) { - content += `- Required Integrations: ${data.constraints.integrations.join(", ")}\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: ${data.constraints.support}\n`; - content += `- Migration Timeline: ${data.constraints.timeline}\n\n`; + content += `- Support Level: ${constraints.support || 'Not specified'}\n`; + content += `- Migration Timeline: ${constraints.timeline || 'Not specified'}\n\n`; - if (data.sortedWeights.length > 0) { + 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`; - data.sortedWeights.forEach((principle: any) => { - content += `| ${principle.name} | ${principle.rubricDescription} | ${ - data.principleWeights[principle.id] + sortedWeights.forEach((principle: any) => { + content += `| ${principle.name || 'Unknown'} | ${principle.rubricDescription || 'No description'} | ${ + principleWeights[principle.id] || 0 } |\n`; }); } @@ -348,121 +421,569 @@ const formatObjectAsMarkdown = (obj: any): string => { .join(" \n"); }; -// Membership Agreement formatting +// Membership Agreement formatting - Complete document with all static and dynamic content const formatMembershipAgreementAsText = (data: any): string => { + const formData = data.formData || {}; let content = `MEMBERSHIP AGREEMENT\n====================\n\n`; - content += `Cooperative Name: ${data.cooperativeName}\n`; - content += `Member Name: ${data.formData.memberName || "[Member Name]"}\n`; - content += `Effective Date: ${data.formData.effectiveDate || "[Date]"}\n\n`; - - if (data.formData.purpose) { - content += `PURPOSE\n-------\n${data.formData.purpose}\n\n`; + + // 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`; } - - if (data.formData.membershipRequirements) { - content += `MEMBERSHIP REQUIREMENTS\n----------------------\n${data.formData.membershipRequirements}\n\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. Consent decision by current members\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 += `Members can leave anytime with ${formData.noticeDays || 30} days notice. The cooperative 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 += `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 += `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 the cooperative\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 the cooperative, 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: ${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`; } - - if (data.formData.rightsAndResponsibilities) { - content += `RIGHTS AND RESPONSIBILITIES\n--------------------------\n${data.formData.rightsAndResponsibilities}\n\n`; + + content += `\nPayment Schedule:\n`; + content += `• Paid on the ${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`; + 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`; + + // Section 8: If We Need to Close + content += `8. IF WE NEED TO CLOSE\n----------------------\n\n`; + content += `If the cooperative 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 Bits + content += `9. LEGAL BITS\n-------------\n\n`; + 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`; + return content; }; const formatMembershipAgreementAsMarkdown = (data: any): string => { - let content = `## Membership Agreement\n\n`; - content += `**Cooperative Name:** ${data.cooperativeName} \n`; - content += `**Member Name:** ${data.formData.memberName || "[Member Name]"} \n`; - content += `**Effective Date:** ${data.formData.effectiveDate || "[Date]"} \n\n`; - - if (data.formData.purpose) { - content += `### Purpose\n\n${data.formData.purpose}\n\n`; + const formData = data.formData || {}; + let content = `# Membership Agreement\n\n`; + + // 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`; } - - if (data.formData.membershipRequirements) { - content += `### Membership Requirements\n\n${data.formData.membershipRequirements}\n\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. Consent decision by current members\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 += `Members can leave anytime with **${formData.noticeDays || 30} days** notice. The cooperative 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 += `### 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 += `### 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 the cooperative\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 the cooperative, 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:** ${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`; } - - if (data.formData.rightsAndResponsibilities) { - content += `### Rights and Responsibilities\n\n${data.formData.rightsAndResponsibilities}\n\n`; + + content += `\n**Payment Schedule:**\n\n`; + content += `- Paid on the **${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`; + 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`; + + // Section 8: If We Need to Close + content += `## 8. If We Need to Close\n\n`; + content += `If the cooperative 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 Bits + content += `## 9. Legal Bits\n\n`; + 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`; + return content; }; -// Conflict Resolution Framework formatting +// 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`; - content += `Organization: ${data.orgName}\n`; - content += `Organization Type: ${data.orgType}\n`; - content += `Member Count: ${data.memberCount}\n\n`; - - if (data.coreValues?.length > 0) { - content += `CORE VALUES\n-----------\n`; - data.coreValues.forEach((value: string, index: number) => { - content += `${index + 1}. ${value}\n`; - }); - content += "\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`; } - - if (data.principles?.length > 0) { - content += `PRINCIPLES\n----------\n`; - data.principles.forEach((principle: string, index: number) => { - content += `${index + 1}. ${principle}\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`; } - - if (data.policies) { - content += `POLICIES\n--------\n`; - Object.entries(data.policies).forEach(([key, value]) => { - if (value) { - const formattedKey = key - .replace(/([A-Z])/g, " $1") - .replace(/^./, (str) => str.toUpperCase()); - content += `${formattedKey}: ${value}\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 => { - let content = `## Conflict Resolution Framework\n\n`; - content += `**Organization:** ${data.orgName} \n`; - content += `**Organization Type:** ${data.orgType} \n`; - content += `**Member Count:** ${data.memberCount} \n\n`; - - if (data.coreValues?.length > 0) { - content += `### Core Values\n\n`; - data.coreValues.forEach((value: string, index: number) => { - content += `${index + 1}. ${value}\n`; - }); - content += "\n"; + 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`; } - - if (data.principles?.length > 0) { - content += `### Principles\n\n`; - data.principles.forEach((principle: string, index: number) => { - content += `${index + 1}. ${principle}\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`; } - - if (data.policies) { - content += `### Policies\n\n`; - Object.entries(data.policies).forEach(([key, value]) => { - if (value) { - const formattedKey = key - .replace(/([A-Z])/g, " $1") - .replace(/^./, (str) => str.toUpperCase()); - content += `**${formattedKey}:** ${value} \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; }; @@ -470,36 +991,36 @@ const formatConflictResolutionAsMarkdown = (data: any): string => { const formatDecisionFrameworkAsText = (data: any): string => { let content = `DECISION FRAMEWORK RESULTS\n=========================\n\n`; - if (data.surveyResponses) { + if (data.surveyResponses && typeof data.surveyResponses === 'object') { content += `SURVEY RESPONSES\n----------------\n`; - content += `Urgency: ${data.surveyResponses.urgency}\n`; - content += `Reversible: ${data.surveyResponses.reversible}\n`; - content += `Expertise: ${data.surveyResponses.expertise}\n`; - content += `Impact: ${data.surveyResponses.impact}\n`; - content += `Options: ${data.surveyResponses.options}\n`; - content += `Investment: ${data.surveyResponses.investment}\n`; - content += `Team Size: ${data.surveyResponses.teamSize}\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) { + if (data.recommendedFramework && typeof data.recommendedFramework === 'object') { const framework = data.recommendedFramework; content += `RECOMMENDED FRAMEWORK\n--------------------\n`; - content += `Method: ${framework.method}\n`; - content += `Tagline: ${framework.tagline}\n\n`; - content += `Reasoning: ${framework.reasoning}\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) { + 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}\n`; + content += `${index + 1}. ${step || '[Step not specified]'}\n`; }); content += "\n"; } - if (framework.tips) { + 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}\n`; + content += `${index + 1}. ${tip || '[Tip not specified]'}\n`; }); content += "\n"; } @@ -511,36 +1032,36 @@ const formatDecisionFrameworkAsText = (data: any): string => { const formatDecisionFrameworkAsMarkdown = (data: any): string => { let content = `## Decision Framework Results\n\n`; - if (data.surveyResponses) { + if (data.surveyResponses && typeof data.surveyResponses === 'object') { content += `### Survey Responses\n\n`; - content += `**Urgency:** ${data.surveyResponses.urgency} \n`; - content += `**Reversible:** ${data.surveyResponses.reversible} \n`; - content += `**Expertise:** ${data.surveyResponses.expertise} \n`; - content += `**Impact:** ${data.surveyResponses.impact} \n`; - content += `**Options:** ${data.surveyResponses.options} \n`; - content += `**Investment:** ${data.surveyResponses.investment} \n`; - content += `**Team Size:** ${data.surveyResponses.teamSize} \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) { + if (data.recommendedFramework && typeof data.recommendedFramework === 'object') { const framework = data.recommendedFramework; content += `### Recommended Framework\n\n`; - content += `**Method:** ${framework.method} \n`; - content += `**Tagline:** ${framework.tagline} \n\n`; - content += `**Reasoning:** ${framework.reasoning}\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) { + 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}\n`; + content += `${index + 1}. ${step || '[Step not specified]'}\n`; }); content += "\n"; } - if (framework.tips) { + 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}\n`; + content += `${index + 1}. ${tip || '[Tip not specified]'}\n`; }); content += "\n"; } diff --git a/components/NeedsCoverageBars.vue b/components/NeedsCoverageBars.vue index 23f1535..984f2b2 100644 --- a/components/NeedsCoverageBars.vue +++ b/components/NeedsCoverageBars.vue @@ -48,20 +48,43 @@ \ No newline at end of file diff --git a/components/RunwayLite.vue b/components/RunwayLite.vue deleted file mode 100644 index 5e03fa1..0000000 --- a/components/RunwayLite.vue +++ /dev/null @@ -1,513 +0,0 @@ - - - \ No newline at end of file diff --git a/components/UnifiedCashFlowDashboard.vue b/components/UnifiedCashFlowDashboard.vue new file mode 100644 index 0000000..7be6e35 --- /dev/null +++ b/components/UnifiedCashFlowDashboard.vue @@ -0,0 +1,255 @@ + + + \ No newline at end of file diff --git a/components/dashboard/AdvancedAccordion.vue b/components/dashboard/AdvancedAccordion.vue index 92deca5..9b29ca4 100644 --- a/components/dashboard/AdvancedAccordion.vue +++ b/components/dashboard/AdvancedAccordion.vue @@ -1,17 +1,18 @@