diff --git a/.DS_Store b/.DS_Store index 550843e..436958c 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/app.vue b/app.vue index 4a1150f..e8aa8ad 100644 --- a/app.vue +++ b/app.vue @@ -91,10 +91,7 @@ const isCoopBuilderSection = computed( route.path === "/dashboard" || route.path === "/mix" || route.path === "/budget" || - route.path === "/runway-lite" || - route.path === "/scenarios" || - route.path === "/cash" || - route.path === "/session" || + route.path === "/cash-flow" || route.path === "/settings" || route.path === "/glossary" ); diff --git a/components/AnnualBudget.vue b/components/AnnualBudget.vue index dface3c..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 cash flow."; + 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 f88d367..c44f5c1 100644 --- a/components/CoopBuilderSubnav.vue +++ b/components/CoopBuilderSubnav.vue @@ -26,45 +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: "scenarios", - name: "Scenarios", - path: "/scenarios", - }, - { - id: "cash", + id: "cash-flow", name: "Cash Flow", - path: "/cash", - }, - { - id: "session", - name: "Value Session", - path: "/session", + path: "/cash-flow", }, ]; diff --git a/components/CoverageChip.vue b/components/CoverageChip.vue index 1fc3225..64e70cb 100644 --- a/components/CoverageChip.vue +++ b/components/CoverageChip.vue @@ -1,11 +1,6 @@