diff --git a/.DS_Store b/.DS_Store index 436958c..550843e 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/app.vue b/app.vue index e8aa8ad..4a1150f 100644 --- a/app.vue +++ b/app.vue @@ -91,7 +91,10 @@ const isCoopBuilderSection = computed( route.path === "/dashboard" || route.path === "/mix" || route.path === "/budget" || - route.path === "/cash-flow" || + route.path === "/runway-lite" || + route.path === "/scenarios" || + route.path === "/cash" || + route.path === "/session" || route.path === "/settings" || route.path === "/glossary" ); diff --git a/components/AnnualBudget.vue b/components/AnnualBudget.vue index 0e8c844..dface3c 100644 --- a/components/AnnualBudget.vue +++ b/components/AnnualBudget.vue @@ -3,18 +3,13 @@

Annual Budget Overview

- -
+ +
- - + + @@ -23,25 +18,24 @@ - + - - + + - + - + @@ -53,15 +47,14 @@ + + + + + - - + + - + - + @@ -99,10 +96,8 @@ - - + + @@ -112,6 +107,7 @@
- Category - - Planned - CategoryPlanned %
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 →

@@ -69,29 +62,33 @@
EXPENSES
- {{ category.name }} -
{{ category.name }} {{ formatCurrency(category.planned) }} {{ category.percentage }}% + {{ category.percentage }}% +
Total ExpensesTotal Expenses {{ formatCurrency(totalExpensesPlanned) }}
NET TOTAL
NET TOTAL {{ formatCurrency(netTotal) }}
+
@@ -122,7 +118,7 @@ interface Props { } const props = withDefaults(defineProps(), { - year: () => new Date().getFullYear(), + year: () => new Date().getFullYear() }); // Get budget data from store @@ -131,54 +127,19 @@ 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; } @@ -186,34 +147,30 @@ 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; } @@ -221,7 +178,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; }); @@ -229,39 +186,33 @@ 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}%). `; @@ -270,141 +221,107 @@ 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 cash flow."; } - + 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}`); }); @@ -419,4 +336,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 deleted file mode 100644 index e50f452..0000000 --- a/components/CashFlowChart.vue +++ /dev/null @@ -1,295 +0,0 @@ - - - \ No newline at end of file diff --git a/components/CoopBuilderSubnav.vue b/components/CoopBuilderSubnav.vue index c44f5c1..f88d367 100644 --- a/components/CoopBuilderSubnav.vue +++ b/components/CoopBuilderSubnav.vue @@ -26,25 +26,45 @@ 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: "cash-flow", + id: "mix", + name: "Revenue Mix", + path: "/mix", + }, + { + id: "runway-lite", + name: "Runway Lite", + path: "/runway-lite", + }, + { + id: "scenarios", + name: "Scenarios", + path: "/scenarios", + }, + { + id: "cash", name: "Cash Flow", - path: "/cash-flow", + path: "/cash", + }, + { + id: "session", + name: "Value Session", + path: "/session", }, ]; diff --git a/components/CoverageChip.vue b/components/CoverageChip.vue index 64e70cb..1fc3225 100644 --- a/components/CoverageChip.vue +++ b/components/CoverageChip.vue @@ -1,6 +1,11 @@