refactor: enhance AnnualBudget component layout with improved dark mode support, streamline table structure, and update CSS for better visual consistency
This commit is contained in:
parent
24e8b7a3a8
commit
f073f91569
14 changed files with 1440 additions and 922 deletions
|
|
@ -237,7 +237,7 @@ html.dark .section-card::before {
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
.dither-shadow {
|
.dither-shadow {
|
||||||
@apply bg-black dark:bg-neutral-600;
|
@apply bg-black dark:bg-neutral-400;
|
||||||
background-image: radial-gradient(white 1px, transparent 1px);
|
background-image: radial-gradient(white 1px, transparent 1px);
|
||||||
background-size: 2px 2px;
|
background-size: 2px 2px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,114 +2,164 @@
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<!-- Annual Budget Overview -->
|
<!-- Annual Budget Overview -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h2 class="text-2xl font-bold">Annual Budget Overview</h2>
|
<div class="relative">
|
||||||
|
<div class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
||||||
|
<div
|
||||||
|
class="relative border border-black dark:border-neutral-600 bg-white dark:bg-neutral-950">
|
||||||
|
<table
|
||||||
|
class="w-full border-collapse text-sm bg-white dark:bg-neutral-950">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-neutral-100 dark:bg-neutral-950">
|
||||||
|
<th
|
||||||
|
class="border-r-1 border-black dark:border-neutral-400 px-4 py-3 text-left font-bold text-black dark:text-white">
|
||||||
|
Category
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="border-r border-neutral-400 dark:border-neutral-600 px-4 py-3 text-right font-bold text-black dark:text-white">
|
||||||
|
Planned
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="px-4 py-3 text-right font-bold text-black dark:text-white">
|
||||||
|
%
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- Revenue Section -->
|
||||||
|
<tr class="bg-black text-white dark:bg-white dark:text-black">
|
||||||
|
<td
|
||||||
|
class="px-4 py-2 font-bold text-white dark:text-black"
|
||||||
|
colspan="3">
|
||||||
|
REVENUE
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<div class="border border-black bg-white">
|
<!-- Revenue Categories -->
|
||||||
<table class="w-full border-collapse text-sm">
|
<tr
|
||||||
<thead>
|
v-for="(category, index) in revenueCategories"
|
||||||
<tr class="border-b-2 border-black bg-neutral-100">
|
:key="`rev-${index}`"
|
||||||
<th class="border-r-1 border-black px-4 py-3 text-left font-bold">
|
class="border-t border-neutral-200 dark:border-neutral-700 text-black dark:text-white"
|
||||||
Category
|
v-show="category.planned > 0">
|
||||||
</th>
|
<td
|
||||||
<th
|
class="border-r-1 border-black dark:border-neutral-400 px-4 py-2 text-black dark:text-white">
|
||||||
class="border-r border-neutral-400 px-4 py-3 text-right font-bold">
|
{{ category.name }}
|
||||||
Planned
|
</td>
|
||||||
</th>
|
<td
|
||||||
<th class="px-4 py-3 text-right font-bold">%</th>
|
class="border-r border-neutral-400 dark:border-neutral-600 px-4 py-2 text-right text-black dark:text-white font-mono">
|
||||||
</tr>
|
{{ formatCurrency(category.planned) }}
|
||||||
</thead>
|
</td>
|
||||||
<tbody>
|
<td
|
||||||
<!-- Revenue Section -->
|
class="px-4 py-2 text-right text-black dark:text-white font-mono">
|
||||||
<tr class="bg-black text-white">
|
{{ category.percentage }}%
|
||||||
<td class="px-4 py-2 font-bold" colspan="3">REVENUE</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Revenue Categories -->
|
<!-- Total Revenue -->
|
||||||
<tr
|
<tr
|
||||||
v-for="(category, index) in revenueCategories"
|
class="border-t-2 border-black dark:border-neutral-400 font-semibold bg-neutral-50 dark:bg-neutral-800">
|
||||||
:key="`rev-${index}`"
|
<td
|
||||||
class="border-t border-neutral-200"
|
class="border-r-1 border-black dark:border-neutral-400 px-4 py-2 text-black dark:text-white">
|
||||||
v-show="category.planned > 0">
|
Total Revenue
|
||||||
<td class="border-r-1 border-black px-4 py-2">
|
</td>
|
||||||
{{ category.name }}
|
<td
|
||||||
</td>
|
class="border-r border-neutral-400 dark:border-neutral-600 px-4 py-2 text-right text-black dark:text-white font-mono">
|
||||||
<td class="border-r border-neutral-400 px-4 py-2 text-right">
|
{{ formatCurrency(totalRevenuePlanned) }}
|
||||||
{{ formatCurrency(category.planned) }}
|
</td>
|
||||||
</td>
|
<td
|
||||||
<td class="px-4 py-2 text-right">{{ category.percentage }}%</td>
|
class="px-4 py-2 text-right text-black dark:text-white font-mono">
|
||||||
</tr>
|
100%
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<!-- Total Revenue -->
|
<!-- Revenue Diversification Guidance -->
|
||||||
<tr class="border-t-2 border-black font-semibold bg-neutral-50">
|
<tr class="bg-neutral-50 dark:bg-neutral-800">
|
||||||
<td class="border-r-1 border-black px-4 py-2">Total Revenue</td>
|
<td
|
||||||
<td class="border-r border-neutral-400 px-4 py-2 text-right">
|
colspan="3"
|
||||||
{{ formatCurrency(totalRevenuePlanned) }}
|
class="border-t border-neutral-300 dark:border-neutral-700 px-4 py-3 text-black dark:text-white">
|
||||||
</td>
|
<div class="text-sm">
|
||||||
<td class="px-4 py-2 text-right">100%</td>
|
<p class="font-medium mb-2 text-black dark:text-white">
|
||||||
</tr>
|
{{ diversificationGuidance }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="text-neutral-600 dark:text-neutral-100 mb-2"
|
||||||
|
v-if="suggestedCategories.length > 0">
|
||||||
|
Consider developing: {{ suggestedCategories.join(", ") }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-black dark:text-white">
|
||||||
|
<NuxtLink
|
||||||
|
to="/help#revenue-diversification"
|
||||||
|
class="text-white dark:text-neutral-100 hover:text-white dark:hover:text-white underline">
|
||||||
|
Learn how to develop these revenue streams →
|
||||||
|
</NuxtLink>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<!-- Revenue Diversification Guidance -->
|
<!-- Expenses Section -->
|
||||||
<tr :class="guidanceBackgroundClass">
|
<tr class="bg-black text-white dark:bg-white dark:text-black">
|
||||||
<td colspan="3" class="border-t border-neutral-300 px-4 py-3">
|
<td
|
||||||
<div class="text-sm">
|
class="px-4 py-2 font-bold text-white dark:text-black"
|
||||||
<p class="font-medium mb-2">{{ diversificationGuidance }}</p>
|
colspan="3">
|
||||||
<p
|
EXPENSES
|
||||||
class="text-neutral-600 mb-2"
|
</td>
|
||||||
v-if="suggestedCategories.length > 0">
|
</tr>
|
||||||
Consider developing: {{ suggestedCategories.join(", ") }}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs">
|
|
||||||
<NuxtLink
|
|
||||||
to="/help#revenue-diversification"
|
|
||||||
class="text-blue-600 hover:text-blue-800 underline">
|
|
||||||
Learn how to develop these revenue streams →
|
|
||||||
</NuxtLink>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<!-- Expenses Section -->
|
<!-- Expense Categories -->
|
||||||
<tr class="bg-black text-white">
|
<tr
|
||||||
<td class="px-4 py-2 font-bold" colspan="3">EXPENSES</td>
|
v-for="(category, index) in expenseCategories"
|
||||||
</tr>
|
:key="`exp-${index}`"
|
||||||
|
class="text-black dark:text-white"
|
||||||
|
v-show="category.planned > 0">
|
||||||
|
<td
|
||||||
|
class="border-r-1 border-black dark:border-neutral-400 px-4 py-2 text-black dark:text-white">
|
||||||
|
{{ category.name }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="border-r border-neutral-400 dark:border-neutral-600 px-4 py-2 text-right text-black dark:text-white font-mono">
|
||||||
|
{{ formatCurrency(category.planned) }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="px-4 py-2 text-right text-black dark:text-white font-mono">
|
||||||
|
{{ category.percentage }}%
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<!-- Expense Categories -->
|
<!-- Total Expenses -->
|
||||||
<tr
|
<tr class="font-semibold bg-neutral-50 dark:bg-neutral-800">
|
||||||
v-for="(category, index) in expenseCategories"
|
<td
|
||||||
:key="`exp-${index}`"
|
class="border-r-1 border-black dark:border-neutral-400 px-4 py-2 text-black dark:text-white">
|
||||||
class="border-t border-neutral-200"
|
Total Expenses
|
||||||
v-show="category.planned > 0">
|
</td>
|
||||||
<td class="border-r-1 border-black px-4 py-2">
|
<td
|
||||||
{{ category.name }}
|
class="border-r border-neutral-400 dark:border-neutral-600 px-4 py-2 text-right text-black dark:text-white font-mono">
|
||||||
</td>
|
{{ formatCurrency(totalExpensesPlanned) }}
|
||||||
<td class="border-r border-neutral-400 px-4 py-2 text-right">
|
</td>
|
||||||
{{ formatCurrency(category.planned) }}
|
<td
|
||||||
</td>
|
class="px-4 py-2 text-right text-black dark:text-white font-mono">
|
||||||
<td class="px-4 py-2 text-right">{{ category.percentage }}%</td>
|
100%
|
||||||
</tr>
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<!-- Total Expenses -->
|
<!-- Net Total -->
|
||||||
<tr class="border-t-2 border-black font-semibold bg-neutral-50">
|
<tr
|
||||||
<td class="border-r-1 border-black px-4 py-2">Total Expenses</td>
|
class="border-t-2 border-black dark:border-neutral-400 font-bold text-lg"
|
||||||
<td class="border-r border-neutral-400 px-4 py-2 text-right">
|
:class="netTotalClass">
|
||||||
{{ formatCurrency(totalExpensesPlanned) }}
|
<td
|
||||||
</td>
|
class="border-r-1 border-black dark:border-neutral-400 px-4 py-3 text-black dark:text-white">
|
||||||
<td class="px-4 py-2 text-right">100%</td>
|
NET TOTAL
|
||||||
</tr>
|
</td>
|
||||||
|
<td
|
||||||
<!-- Net Total -->
|
class="border-r border-neutral-400 dark:border-neutral-600 px-4 py-3 text-right text-black dark:text-white font-mono">
|
||||||
<tr
|
{{ formatCurrency(netTotal) }}
|
||||||
class="border-t-2 border-black font-bold text-lg"
|
</td>
|
||||||
:class="netTotalClass">
|
<td class="px-4 py-3 text-right text-black dark:text-white">
|
||||||
<td class="border-r-1 border-black px-4 py-3">NET TOTAL</td>
|
-
|
||||||
<td class="border-r border-neutral-400 px-4 py-3 text-right">
|
</td>
|
||||||
{{ formatCurrency(netTotal) }}
|
</tr>
|
||||||
</td>
|
</tbody>
|
||||||
<td class="px-4 py-3 text-right">-</td>
|
</table>
|
||||||
</tr>
|
</div>
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -242,9 +292,9 @@ const netTotal = computed(
|
||||||
);
|
);
|
||||||
|
|
||||||
const netTotalClass = computed(() => {
|
const netTotalClass = computed(() => {
|
||||||
if (netTotal.value > 0) return "bg-green-50";
|
if (netTotal.value > 0) return "bg-green-50 dark:bg-green-950";
|
||||||
if (netTotal.value < 0) return "bg-red-50";
|
if (netTotal.value < 0) return "bg-red-50 dark:bg-red-950";
|
||||||
return "bg-neutral-50";
|
return "bg-neutral-50 dark:bg-neutral-800";
|
||||||
});
|
});
|
||||||
|
|
||||||
// Diversification guidance
|
// Diversification guidance
|
||||||
|
|
@ -308,28 +358,6 @@ const diversificationGuidance = computed(() => {
|
||||||
return guidance;
|
return guidance;
|
||||||
});
|
});
|
||||||
|
|
||||||
const guidanceBackgroundClass = computed(() => {
|
|
||||||
const topCategory = revenueCategories.value.reduce(
|
|
||||||
(max, cat) => (cat.percentage > max.percentage ? cat : max),
|
|
||||||
{ percentage: 0 }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (topCategory.percentage >= 70) {
|
|
||||||
return "bg-red-50";
|
|
||||||
} else if (topCategory.percentage >= 50) {
|
|
||||||
return "bg-red-50";
|
|
||||||
} else {
|
|
||||||
const categoriesAbove20 = revenueCategories.value.filter(
|
|
||||||
(cat) => cat.percentage >= 20
|
|
||||||
).length;
|
|
||||||
if (categoriesAbove20 >= 3) {
|
|
||||||
return "bg-green-50";
|
|
||||||
} else {
|
|
||||||
return "bg-yellow-50";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Suggested categories to develop
|
// Suggested categories to develop
|
||||||
const suggestedCategories = computed(() => {
|
const suggestedCategories = computed(() => {
|
||||||
const categoriesWithRevenue = revenueCategories.value.filter(
|
const categoriesWithRevenue = revenueCategories.value.filter(
|
||||||
|
|
@ -394,10 +422,11 @@ function formatCurrency(amount: number): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPercentageClass(percentage: number): string {
|
function getPercentageClass(percentage: number): string {
|
||||||
if (percentage > 50) return "text-red-600 font-bold";
|
if (percentage > 50) return "text-red-600 dark:text-red-400 font-bold";
|
||||||
if (percentage > 35) return "text-yellow-600 font-semibold";
|
if (percentage > 35)
|
||||||
if (percentage > 20) return "text-black font-medium";
|
return "text-yellow-600 dark:text-yellow-400 font-semibold";
|
||||||
return "text-neutral-500";
|
if (percentage > 20) return "text-black dark:text-white font-medium";
|
||||||
|
return "text-neutral-500 dark:text-neutral-400";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
|
|
|
||||||
208
components/BudgetSettingsModal.vue
Normal file
208
components/BudgetSettingsModal.vue
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
<template>
|
||||||
|
<UModal
|
||||||
|
v-model:open="isOpen"
|
||||||
|
title="Budget Settings"
|
||||||
|
description="Configure payroll taxes and cash flow settings"
|
||||||
|
:dismissible="true">
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-8">
|
||||||
|
<!-- Payroll Tax Settings -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">
|
||||||
|
Payroll Tax Percentage
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
|
||||||
|
Set the percentage added to base payroll for taxes, benefits, and other oncosts.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<UFormField label="Tax Percentage">
|
||||||
|
<UInput
|
||||||
|
v-model="newOncostPct"
|
||||||
|
type="number"
|
||||||
|
placeholder="Enter percentage"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="0.1"
|
||||||
|
size="lg"
|
||||||
|
class="text-lg">
|
||||||
|
<template #trailing>
|
||||||
|
<span class="text-neutral-500 text-lg">%</span>
|
||||||
|
</template>
|
||||||
|
</UInput>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<!-- Quick selection buttons -->
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<span class="text-sm text-neutral-600 dark:text-neutral-400 mr-2">Quick select:</span>
|
||||||
|
<UButton
|
||||||
|
v-for="range in commonOncostRanges"
|
||||||
|
:key="range.value"
|
||||||
|
@click="newOncostPct = range.value"
|
||||||
|
size="xs"
|
||||||
|
variant="outline"
|
||||||
|
:ui="{ base: 'hover:bg-neutral-100 dark:hover:bg-neutral-800' }">
|
||||||
|
{{ range.value }}%
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview -->
|
||||||
|
<div class="p-3 bg-neutral-100 dark:bg-neutral-800 rounded">
|
||||||
|
<p class="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
<strong>Example:</strong> €100 base payroll + {{ newOncostPct }}% tax =
|
||||||
|
<strong>€{{ Math.round(100 * (1 + newOncostPct / 100)) }} total cost</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Minimum Cash Threshold Settings -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">
|
||||||
|
Minimum Cash Threshold
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
|
||||||
|
Set the minimum cash balance that your co-op should never go below. The budget will automatically reduce payroll allocations if needed to maintain this threshold.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<UFormField label="Minimum Cash Balance">
|
||||||
|
<UInput
|
||||||
|
v-model="newMinCashThreshold"
|
||||||
|
type="number"
|
||||||
|
placeholder="Enter minimum amount"
|
||||||
|
min="0"
|
||||||
|
step="100"
|
||||||
|
size="lg"
|
||||||
|
class="text-lg">
|
||||||
|
<template #leading>
|
||||||
|
<span class="text-neutral-500 text-lg">{{ getCurrencySymbol(coopStore.currency) }}</span>
|
||||||
|
</template>
|
||||||
|
</UInput>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<!-- Quick selection buttons -->
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<span class="text-sm text-neutral-600 dark:text-neutral-400 mr-2">Quick select:</span>
|
||||||
|
<UButton
|
||||||
|
v-for="amount in commonCashThresholds"
|
||||||
|
:key="amount"
|
||||||
|
@click="newMinCashThreshold = amount"
|
||||||
|
size="xs"
|
||||||
|
variant="outline"
|
||||||
|
:ui="{ base: 'hover:bg-neutral-100 dark:hover:bg-neutral-800' }">
|
||||||
|
{{ getCurrencySymbol(coopStore.currency) }}{{ formatAmount(amount) }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview -->
|
||||||
|
<div class="p-3 bg-neutral-100 dark:bg-neutral-800 rounded">
|
||||||
|
<p class="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
|
<strong>Safety buffer:</strong> Your co-op will maintain at least
|
||||||
|
<strong>{{ getCurrencySymbol(coopStore.currency) }}{{ formatAmount(newMinCashThreshold) }}</strong>
|
||||||
|
in cash at all times.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-between items-center w-full">
|
||||||
|
<UButton
|
||||||
|
@click="resetToDefaults"
|
||||||
|
variant="ghost"
|
||||||
|
color="neutral"
|
||||||
|
:ui="{ base: 'hover:bg-neutral-100 dark:hover:bg-neutral-800' }">
|
||||||
|
Reset to Defaults
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UButton
|
||||||
|
@click="isOpen = false"
|
||||||
|
variant="outline"
|
||||||
|
color="neutral">
|
||||||
|
Cancel
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
@click="saveSettings"
|
||||||
|
:disabled="!isValidSettings"
|
||||||
|
color="primary">
|
||||||
|
Save Settings
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getCurrencySymbol } from "~/utils/currency";
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const isOpen = defineModel<boolean>("open", { default: false });
|
||||||
|
|
||||||
|
// Emit events for parent component
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"settings-updated": [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// Stores
|
||||||
|
const coopStore = useCoopBuilderStore();
|
||||||
|
|
||||||
|
// Reactive form data
|
||||||
|
const newOncostPct = ref(coopStore.payrollOncostPct || 25);
|
||||||
|
const newMinCashThreshold = ref(coopStore.minCashThreshold || 0);
|
||||||
|
|
||||||
|
// Common ranges for quick selection
|
||||||
|
const commonOncostRanges = [
|
||||||
|
{ value: 0 },
|
||||||
|
{ value: 15 },
|
||||||
|
{ value: 20 },
|
||||||
|
{ value: 25 },
|
||||||
|
{ value: 30 },
|
||||||
|
{ value: 35 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const commonCashThresholds = [0, 1000, 2500, 5000, 7500, 10000];
|
||||||
|
|
||||||
|
// Computed properties
|
||||||
|
const isValidSettings = computed(() =>
|
||||||
|
newOncostPct.value >= 0 &&
|
||||||
|
newOncostPct.value <= 100 &&
|
||||||
|
newMinCashThreshold.value >= 0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Utility function to format amounts
|
||||||
|
function formatAmount(amount: number): string {
|
||||||
|
return new Intl.NumberFormat().format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset form when modal opens
|
||||||
|
watch(() => isOpen.value, (open) => {
|
||||||
|
if (open) {
|
||||||
|
newOncostPct.value = coopStore.payrollOncostPct || 25;
|
||||||
|
newMinCashThreshold.value = coopStore.minCashThreshold || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetToDefaults() {
|
||||||
|
newOncostPct.value = 25;
|
||||||
|
newMinCashThreshold.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
// Update payroll oncost percentage
|
||||||
|
coopStore.payrollOncostPct = newOncostPct.value;
|
||||||
|
|
||||||
|
// Update minimum cash threshold
|
||||||
|
coopStore.setMinCashThreshold(newMinCashThreshold.value);
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
isOpen.value = false;
|
||||||
|
|
||||||
|
// Emit event to parent to refresh calculations
|
||||||
|
emit("settings-updated");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -1,323 +0,0 @@
|
||||||
<template>
|
|
||||||
<UModal
|
|
||||||
v-model:open="isOpen"
|
|
||||||
title="Payroll Oncost Settings"
|
|
||||||
description="Configure payroll taxes and benefits percentage"
|
|
||||||
:dismissible="true">
|
|
||||||
<template #body>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<!-- Explanation -->
|
|
||||||
<div class="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
|
|
||||||
<div class="flex items-start">
|
|
||||||
<UIcon
|
|
||||||
name="i-heroicons-information-circle"
|
|
||||||
class="h-5 w-5 text-blue-400 mt-0.5 mr-3 flex-shrink-0" />
|
|
||||||
<div class="text-sm">
|
|
||||||
<p class="text-blue-800 dark:text-blue-200 font-medium mb-2">
|
|
||||||
What are payroll oncosts?
|
|
||||||
</p>
|
|
||||||
<p class="text-blue-700 dark:text-blue-300">
|
|
||||||
Payroll oncosts cover taxes, benefits, and other
|
|
||||||
employee-related expenses beyond base wages. This typically
|
|
||||||
includes employer payroll taxes, worker's compensation, benefits
|
|
||||||
contributions, and other statutory requirements.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Current Settings Display -->
|
|
||||||
<div class="bg-neutral-50 dark:bg-neutral-800 p-4 rounded-lg">
|
|
||||||
<h4 class="font-medium text-neutral-900 dark:text-white mb-3">
|
|
||||||
Current Impact
|
|
||||||
</h4>
|
|
||||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<div class="text-neutral-600 dark:text-neutral-400">
|
|
||||||
Base Payroll
|
|
||||||
</div>
|
|
||||||
<div class="font-medium">
|
|
||||||
{{ formatCurrency(basePayroll) }}/month
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-neutral-600 dark:text-neutral-400">
|
|
||||||
Oncosts ({{ currentOncostPct }}%)
|
|
||||||
</div>
|
|
||||||
<div class="font-medium">
|
|
||||||
{{ formatCurrency(currentOncostAmount) }}/month
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mt-3 pt-3 border-t border-neutral-200 dark:border-neutral-700">
|
|
||||||
<div class="text-neutral-600 dark:text-neutral-400 text-sm">
|
|
||||||
Total Payroll Cost
|
|
||||||
</div>
|
|
||||||
<div class="font-semibold text-lg">
|
|
||||||
{{ formatCurrency(totalPayrollCost) }}/month
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Percentage Input -->
|
|
||||||
<div class="space-y-3">
|
|
||||||
<label
|
|
||||||
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
|
||||||
Oncost Percentage
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="flex items-center space-x-3">
|
|
||||||
<div class="flex-1">
|
|
||||||
<UInput
|
|
||||||
v-model.number="newOncostPct"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
step="1"
|
|
||||||
placeholder="25"
|
|
||||||
class="text-center" />
|
|
||||||
</div>
|
|
||||||
<span class="text-sm text-neutral-500">%</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Slider for easier adjustment -->
|
|
||||||
<div class="space-y-2">
|
|
||||||
<input
|
|
||||||
v-model.number="newOncostPct"
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="50"
|
|
||||||
step="1"
|
|
||||||
class="w-full h-2 bg-neutral-200 rounded-lg appearance-none cursor-pointer dark:bg-neutral-700 slider" />
|
|
||||||
<div class="flex justify-between text-xs text-neutral-500">
|
|
||||||
<span>0%</span>
|
|
||||||
<span>25%</span>
|
|
||||||
<span>50%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Preview of New Settings -->
|
|
||||||
<div
|
|
||||||
v-if="newOncostPct !== currentOncostPct"
|
|
||||||
class="bg-green-50 dark:bg-green-900/20 p-4 rounded-lg">
|
|
||||||
<h4 class="font-medium text-green-800 dark:text-green-200 mb-3">
|
|
||||||
Preview Changes
|
|
||||||
</h4>
|
|
||||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<div class="text-green-700 dark:text-green-300">
|
|
||||||
New Oncosts ({{ newOncostPct }}%)
|
|
||||||
</div>
|
|
||||||
<div class="font-medium">
|
|
||||||
{{ formatCurrency(newOncostAmount) }}/month
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-green-700 dark:text-green-300">
|
|
||||||
New Total Cost
|
|
||||||
</div>
|
|
||||||
<div class="font-medium">
|
|
||||||
{{ formatCurrency(newTotalCost) }}/month
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-xs">
|
|
||||||
<span class="text-green-700 dark:text-green-300">
|
|
||||||
{{ newTotalCost > totalPayrollCost ? "Increase" : "Decrease" }} of
|
|
||||||
{{
|
|
||||||
formatCurrency(Math.abs(newTotalCost - totalPayrollCost))
|
|
||||||
}}/month
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Common Oncost Ranges -->
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label
|
|
||||||
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
|
||||||
Common Ranges
|
|
||||||
</label>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<UButton
|
|
||||||
v-for="preset in commonRanges"
|
|
||||||
:key="preset.value"
|
|
||||||
size="xs"
|
|
||||||
color="neutral"
|
|
||||||
variant="outline"
|
|
||||||
@click="newOncostPct = preset.value">
|
|
||||||
{{ preset.label }}
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #footer="{ close }">
|
|
||||||
<div class="flex justify-end gap-3">
|
|
||||||
<UButton color="neutral" variant="ghost" @click="handleCancel">
|
|
||||||
Cancel
|
|
||||||
</UButton>
|
|
||||||
<UButton
|
|
||||||
color="primary"
|
|
||||||
@click="handleSave"
|
|
||||||
:disabled="!isValidPercentage">
|
|
||||||
Update Oncost Percentage
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</UModal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { allocatePayroll as allocatePayrollImpl } from "~/types/members";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
open: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Emits {
|
|
||||||
(e: "update:open", value: boolean): void;
|
|
||||||
(e: "save", percentage: number): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
const emit = defineEmits<Emits>();
|
|
||||||
|
|
||||||
// Modal state
|
|
||||||
const isOpen = computed({
|
|
||||||
get: () => props.open,
|
|
||||||
set: (value) => emit("update:open", value),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get current payroll data
|
|
||||||
const coopStore = useCoopBuilderStore();
|
|
||||||
const currentOncostPct = computed(() => coopStore.payrollOncostPct || 0);
|
|
||||||
|
|
||||||
// Calculate current payroll values using the same logic as the budget store
|
|
||||||
const { allocatePayroll } = useCoopBuilder();
|
|
||||||
|
|
||||||
const basePayroll = computed(() => {
|
|
||||||
// Calculate base payroll the same way the budget store does
|
|
||||||
const totalHours = coopStore.members.reduce(
|
|
||||||
(sum, m) =>
|
|
||||||
sum + (m.hoursPerMonth || (m.hoursPerWeek ? m.hoursPerWeek * 4.33 : 0)),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const hourlyWage = coopStore.equalHourlyWage || 0;
|
|
||||||
const basePayrollBudget = totalHours * hourlyWage;
|
|
||||||
|
|
||||||
if (basePayrollBudget > 0 && coopStore.members.length > 0) {
|
|
||||||
// Use policy-driven allocation to get actual member pay amounts
|
|
||||||
const payPolicy = {
|
|
||||||
relationship: coopStore.policy.relationship,
|
|
||||||
roleBands: coopStore.policy.roleBands,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert members to the format expected by allocatePayroll
|
|
||||||
const membersForAllocation = coopStore.members.map((m) => ({
|
|
||||||
...m,
|
|
||||||
displayName: m.name,
|
|
||||||
monthlyPayPlanned: m.monthlyPayPlanned || 0,
|
|
||||||
minMonthlyNeeds: m.minMonthlyNeeds || 0,
|
|
||||||
hoursPerMonth:
|
|
||||||
m.hoursPerMonth || (m.hoursPerWeek ? m.hoursPerWeek * 4.33 : 0),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Use the imported allocatePayroll function
|
|
||||||
const allocatedMembers = allocatePayrollImpl(
|
|
||||||
membersForAllocation,
|
|
||||||
payPolicy,
|
|
||||||
basePayrollBudget
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sum the allocated amounts for total payroll
|
|
||||||
return allocatedMembers.reduce(
|
|
||||||
(sum, m) => sum + (m.monthlyPayPlanned || 0),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentOncostAmount = computed(
|
|
||||||
() => basePayroll.value * (currentOncostPct.value / 100)
|
|
||||||
);
|
|
||||||
|
|
||||||
const totalPayrollCost = computed(
|
|
||||||
() => basePayroll.value + currentOncostAmount.value
|
|
||||||
);
|
|
||||||
|
|
||||||
// New percentage input
|
|
||||||
const newOncostPct = ref(currentOncostPct.value);
|
|
||||||
|
|
||||||
// Computed values for preview
|
|
||||||
const newOncostAmount = computed(
|
|
||||||
() => basePayroll.value * (newOncostPct.value / 100)
|
|
||||||
);
|
|
||||||
const newTotalCost = computed(() => basePayroll.value + newOncostAmount.value);
|
|
||||||
|
|
||||||
const isValidPercentage = computed(
|
|
||||||
() => newOncostPct.value >= 0 && newOncostPct.value <= 100
|
|
||||||
);
|
|
||||||
|
|
||||||
// Common oncost ranges
|
|
||||||
const commonRanges = [
|
|
||||||
{ label: "0% (No oncosts)", value: 0 },
|
|
||||||
{ label: "15% (Basic)", value: 15 },
|
|
||||||
{ label: "25% (Standard)", value: 25 },
|
|
||||||
{ label: "35% (Comprehensive)", value: 35 },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Reset to current value when modal opens
|
|
||||||
watch(isOpen, (open) => {
|
|
||||||
if (open) {
|
|
||||||
newOncostPct.value = currentOncostPct.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handlers
|
|
||||||
function handleCancel() {
|
|
||||||
newOncostPct.value = currentOncostPct.value;
|
|
||||||
isOpen.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSave() {
|
|
||||||
if (isValidPercentage.value) {
|
|
||||||
emit("save", newOncostPct.value);
|
|
||||||
isOpen.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Currency formatting
|
|
||||||
function formatCurrency(amount: number): string {
|
|
||||||
return new Intl.NumberFormat("en-US", {
|
|
||||||
style: "currency",
|
|
||||||
currency: "USD",
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
maximumFractionDigits: 0,
|
|
||||||
}).format(amount);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.slider::-webkit-slider-thumb {
|
|
||||||
appearance: none;
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #3b82f6;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider::-moz-range-thumb {
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #3b82f6;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -102,7 +102,7 @@ export function useCoopBuilder() {
|
||||||
streams: baseStreams.map(s => {
|
streams: baseStreams.map(s => {
|
||||||
// Reduce service revenue by 30%
|
// Reduce service revenue by 30%
|
||||||
if (s.category?.toLowerCase().includes('service') || s.label.toLowerCase().includes('service')) {
|
if (s.category?.toLowerCase().includes('service') || s.label.toLowerCase().includes('service')) {
|
||||||
return { ...s, monthly: (s.monthly || 0) * 0.7 }
|
return { ...s, monthly: Math.round((s.monthly || 0) * 0.7) }
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
|
|
@ -128,7 +128,7 @@ export function useCoopBuilder() {
|
||||||
if (revenueDelay > 0) {
|
if (revenueDelay > 0) {
|
||||||
adjustedStreams = adjustedStreams.map(s => ({
|
adjustedStreams = adjustedStreams.map(s => ({
|
||||||
...s,
|
...s,
|
||||||
monthly: (s.monthly || 0) * Math.max(0, 1 - (revenueDelay / 12))
|
monthly: Math.round((s.monthly || 0) * Math.max(0, 1 - (revenueDelay / 12)))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,7 +251,7 @@ export function useCoopBuilder() {
|
||||||
// Calculate monthly payroll
|
// Calculate monthly payroll
|
||||||
const payrollCost = monthlyPayroll(scenarioMembers || [], currentMode) || 0
|
const payrollCost = monthlyPayroll(scenarioMembers || [], currentMode) || 0
|
||||||
const oncostPct = store.payrollOncostPct || 0
|
const oncostPct = store.payrollOncostPct || 0
|
||||||
const totalPayroll = payrollCost * (1 + Math.max(0, oncostPct) / 100)
|
const totalPayroll = Math.round(payrollCost * (1 + Math.max(0, oncostPct) / 100))
|
||||||
|
|
||||||
// Calculate revenue and costs
|
// Calculate revenue and costs
|
||||||
const totalRevenue = (scenarioStreams || []).reduce((sum, s) => sum + (s.monthly || 0), 0)
|
const totalRevenue = (scenarioStreams || []).reduce((sum, s) => sum + (s.monthly || 0), 0)
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export function useCushionForecast() {
|
||||||
const operatingMode = policiesStore.operatingMode || 'minimum'
|
const operatingMode = policiesStore.operatingMode || 'minimum'
|
||||||
const payrollCost = monthlyPayroll(membersStore.members, operatingMode)
|
const payrollCost = monthlyPayroll(membersStore.members, operatingMode)
|
||||||
const oncostPct = policiesStore.payrollOncostPct || 0
|
const oncostPct = policiesStore.payrollOncostPct || 0
|
||||||
const totalPayroll = payrollCost * (1 + oncostPct / 100)
|
const totalPayroll = Math.round(payrollCost * (1 + oncostPct / 100))
|
||||||
const overheadCost = budgetStore.overheadCosts.reduce((sum, cost) => sum + (cost.amount || 0), 0)
|
const overheadCost = budgetStore.overheadCosts.reduce((sum, cost) => sum + (cost.amount || 0), 0)
|
||||||
|
|
||||||
return totalPayroll + overheadCost
|
return totalPayroll + overheadCost
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export const useDeferredMetrics = () => {
|
||||||
): number => {
|
): number => {
|
||||||
const totalTargetHours = members.reduce((sum, member) => sum + (member.targetHours || 0), 0)
|
const totalTargetHours = members.reduce((sum, member) => sum + (member.targetHours || 0), 0)
|
||||||
const grossPayroll = totalTargetHours * hourlyWage
|
const grossPayroll = totalTargetHours * hourlyWage
|
||||||
return grossPayroll * (1 + oncostPct / 100)
|
return Math.round(grossPayroll * (1 + oncostPct / 100))
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculateDeferredRatio = (
|
const calculateDeferredRatio = (
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export function usePayrollAllocation() {
|
||||||
|
|
||||||
// Total payroll with oncosts
|
// Total payroll with oncosts
|
||||||
const totalPayrollWithOncosts = computed(() => {
|
const totalPayrollWithOncosts = computed(() => {
|
||||||
return basePayrollBudget.value * (1 + payrollOncostPct.value / 100)
|
return Math.round(basePayrollBudget.value * (1 + payrollOncostPct.value / 100))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update member planned pay when allocation changes
|
// Update member planned pay when allocation changes
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export const useRunway = () => {
|
||||||
|
|
||||||
// Add oncosts
|
// Add oncosts
|
||||||
const oncostPct = policiesStore.payrollOncostPct || 0
|
const oncostPct = policiesStore.payrollOncostPct || 0
|
||||||
const totalPayroll = payrollCost * (1 + oncostPct / 100)
|
const totalPayroll = Math.round(payrollCost * (1 + oncostPct / 100))
|
||||||
|
|
||||||
// Add overhead costs
|
// Add overhead costs
|
||||||
const overheadCost = budgetStore.overheadCosts.reduce((sum, cost) => sum + (cost.amount || 0), 0)
|
const overheadCost = budgetStore.overheadCosts.reduce((sum, cost) => sum + (cost.amount || 0), 0)
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ export const useStoreSync = () => {
|
||||||
id: member.id,
|
id: member.id,
|
||||||
name: member.displayName,
|
name: member.displayName,
|
||||||
role: member.role,
|
role: member.role,
|
||||||
hoursPerMonth: Number(((member.hoursPerWeek || 0) * 4.33).toFixed(2)),
|
hoursPerMonth: Math.round((member.hoursPerWeek || 0) * 4.33),
|
||||||
minMonthlyNeeds: member.minMonthlyNeeds,
|
minMonthlyNeeds: member.minMonthlyNeeds,
|
||||||
monthlyPayPlanned: member.monthlyPayPlanned,
|
monthlyPayPlanned: member.monthlyPayPlanned,
|
||||||
targetMonthlyPay: member.targetMonthlyPay,
|
targetMonthlyPay: member.targetMonthlyPay,
|
||||||
|
|
|
||||||
1305
pages/budget.vue
1305
pages/budget.vue
File diff suppressed because it is too large
Load diff
|
|
@ -1,46 +1,56 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="template-container min-h-screen bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100 py-8"
|
class="template-container min-h-screen bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100 py-8">
|
||||||
>
|
|
||||||
<div class="max-w-6xl mx-auto px-4 relative">
|
<div class="max-w-6xl mx-auto px-4 relative">
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="text-3xl font-bold text-neutral-900 dark:text-white mb-2">
|
<h1 class="text-3xl font-bold text-neutral-900 dark:text-white mb-2">
|
||||||
More Resources & Templates
|
More Resources & Templates
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-neutral-700 dark:text-neutral-200">
|
<p class="text-neutral-700 dark:text-neutral-200">
|
||||||
Additional tools, templates, and resources to support your cooperative journey.
|
Additional tools, templates, and resources to support your
|
||||||
|
cooperative journey.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<!-- External Templates Section -->
|
<!-- External Templates Section -->
|
||||||
<section>
|
<section>
|
||||||
<h2 class="text-2xl font-semibold text-neutral-900 dark:text-white mb-4">
|
<h2
|
||||||
|
class="text-2xl font-semibold text-neutral-900 dark:text-white mb-4">
|
||||||
External Templates
|
External Templates
|
||||||
</h2>
|
</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<!-- Miro Template -->
|
<!-- Miro Template -->
|
||||||
<div class="template-card">
|
<div class="template-card">
|
||||||
<div class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
|
||||||
<div
|
<div
|
||||||
class="relative bg-white dark:bg-neutral-950 border border-black dark:border-white p-6"
|
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
||||||
>
|
<div
|
||||||
<h3 class="text-xl font-semibold text-neutral-900 dark:text-white mb-2">
|
class="relative bg-white dark:bg-neutral-950 border border-black dark:border-white p-6">
|
||||||
|
<h3
|
||||||
|
class="text-xl font-semibold text-neutral-900 dark:text-white mb-2">
|
||||||
Goals & Values Exercise
|
Goals & Values Exercise
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-neutral-700 dark:text-neutral-200 mb-4">
|
<p class="text-neutral-700 dark:text-neutral-200 mb-4">
|
||||||
A Miro template to help your team align on shared goals and values through collaborative exercises.
|
A Miro template to help your team align on shared goals and
|
||||||
|
values through collaborative exercises.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href="https://miro.com/miroverse/goals-values-exercise/"
|
href="https://miro.com/miroverse/goals-values-exercise/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="inline-flex items-center px-4 py-2 bg-black dark:bg-white text-white dark:text-black font-medium hover:opacity-90 transition-opacity"
|
class="inline-flex items-center px-4 py-2 bg-black dark:bg-white text-white dark:text-black font-medium hover:opacity-90 transition-opacity">
|
||||||
>
|
|
||||||
Open in Miro
|
Open in Miro
|
||||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
class="w-4 h-4 ml-2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -48,25 +58,34 @@
|
||||||
|
|
||||||
<!-- CommunityRule Templates -->
|
<!-- CommunityRule Templates -->
|
||||||
<div class="template-card">
|
<div class="template-card">
|
||||||
<div class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
|
||||||
<div
|
<div
|
||||||
class="relative bg-white dark:bg-neutral-950 border border-black dark:border-white p-6"
|
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
||||||
>
|
<div
|
||||||
<h3 class="text-xl font-semibold text-neutral-900 dark:text-white mb-2">
|
class="relative bg-white dark:bg-neutral-950 border border-black dark:border-white p-6">
|
||||||
|
<h3
|
||||||
|
class="text-xl font-semibold text-neutral-900 dark:text-white mb-2">
|
||||||
CommunityRule Governance Templates
|
CommunityRule Governance Templates
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-neutral-700 dark:text-neutral-200 mb-4">
|
<p class="text-neutral-700 dark:text-neutral-200 mb-4">
|
||||||
A collection of governance templates and patterns for democratic organizations and communities.
|
A collection of governance templates and patterns for
|
||||||
|
democratic organizations and communities.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href="https://communityrule.info/"
|
href="https://communityrule.info/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="inline-flex items-center px-4 py-2 bg-black dark:bg-white text-white dark:text-black font-medium hover:opacity-90 transition-opacity"
|
class="inline-flex items-center px-4 py-2 bg-black dark:bg-white text-white dark:text-black font-medium hover:opacity-90 transition-opacity">
|
||||||
>
|
|
||||||
Browse Templates
|
Browse Templates
|
||||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002 2v-4M14 4h6m0 0v6m0-6L10 14" />
|
class="w-4 h-4 ml-2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002 2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -76,7 +95,8 @@
|
||||||
|
|
||||||
<!-- PDF Downloads Section -->
|
<!-- PDF Downloads Section -->
|
||||||
<section>
|
<section>
|
||||||
<h2 class="text-2xl font-semibold text-neutral-900 dark:text-white mb-4">
|
<h2
|
||||||
|
class="text-2xl font-semibold text-neutral-900 dark:text-white mb-4">
|
||||||
Wizard PDF Downloads
|
Wizard PDF Downloads
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-neutral-700 dark:text-neutral-200 mb-4">
|
<p class="text-neutral-700 dark:text-neutral-200 mb-4">
|
||||||
|
|
@ -86,21 +106,23 @@
|
||||||
<div
|
<div
|
||||||
v-for="pdf in pdfDownloads"
|
v-for="pdf in pdfDownloads"
|
||||||
:key="pdf.id"
|
:key="pdf.id"
|
||||||
class="template-card"
|
class="template-card">
|
||||||
>
|
|
||||||
<div class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
|
||||||
<div
|
<div
|
||||||
class="relative bg-white dark:bg-neutral-950 border border-black dark:border-white p-6"
|
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
||||||
>
|
<div
|
||||||
|
class="relative bg-white dark:bg-neutral-950 border border-black dark:border-white p-6">
|
||||||
<div class="flex items-start justify-between mb-2">
|
<div class="flex items-start justify-between mb-2">
|
||||||
<h3 class="text-lg font-semibold text-neutral-900 dark:text-white">
|
<h3
|
||||||
|
class="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||||
{{ pdf.name }}
|
{{ pdf.name }}
|
||||||
</h3>
|
</h3>
|
||||||
<span class="text-xs text-neutral-600 dark:text-neutral-400 bg-neutral-100 dark:bg-neutral-800 px-2 py-1 rounded">
|
<span
|
||||||
|
class="text-xs text-neutral-600 dark:text-neutral-400 bg-neutral-100 dark:bg-neutral-800 px-2 py-1 rounded">
|
||||||
PDF
|
PDF
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-neutral-700 dark:text-neutral-200 mb-4">
|
<p
|
||||||
|
class="text-sm text-neutral-700 dark:text-neutral-200 mb-4">
|
||||||
{{ pdf.description }}
|
{{ pdf.description }}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
|
|
@ -109,13 +131,20 @@
|
||||||
'inline-flex items-center px-4 py-2 font-medium transition-opacity',
|
'inline-flex items-center px-4 py-2 font-medium transition-opacity',
|
||||||
pdf.available
|
pdf.available
|
||||||
? 'bg-black dark:bg-white text-white dark:text-black hover:opacity-90'
|
? 'bg-black dark:bg-white text-white dark:text-black hover:opacity-90'
|
||||||
: 'bg-neutral-200 dark:bg-neutral-800 text-neutral-500 dark:text-neutral-500 cursor-not-allowed'
|
: 'bg-neutral-200 dark:bg-neutral-800 text-neutral-500 dark:text-neutral-500 cursor-not-allowed',
|
||||||
]"
|
]">
|
||||||
>
|
<svg
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
class="w-4 h-4 mr-2"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
{{ pdf.available ? 'Download' : 'Coming Soon' }}
|
{{ pdf.available ? "Download" : "Coming Soon" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -131,48 +160,48 @@
|
||||||
// PDF downloads list with placeholder data
|
// PDF downloads list with placeholder data
|
||||||
const pdfDownloads = [
|
const pdfDownloads = [
|
||||||
{
|
{
|
||||||
id: 'bylaws',
|
id: "bylaws",
|
||||||
name: 'Bylaws Wizard',
|
name: "Bylaws Wizard",
|
||||||
description: 'Create comprehensive bylaws for your cooperative',
|
description: "Create comprehensive bylaws for your cooperative",
|
||||||
available: false,
|
available: false,
|
||||||
url: '#'
|
url: "#",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'operating-agreement',
|
id: "operating-agreement",
|
||||||
name: 'Operating Agreement Wizard',
|
name: "Operating Agreement Wizard",
|
||||||
description: 'Draft an operating agreement for your LLC cooperative',
|
description: "Draft an operating agreement for your LLC cooperative",
|
||||||
available: false,
|
available: false,
|
||||||
url: '#'
|
url: "#",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'articles',
|
id: "articles",
|
||||||
name: 'Articles of Incorporation',
|
name: "Articles of Incorporation",
|
||||||
description: 'Template for articles of incorporation',
|
description: "Template for articles of incorporation",
|
||||||
available: false,
|
available: false,
|
||||||
url: '#'
|
url: "#",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'membership',
|
id: "membership",
|
||||||
name: 'Membership Agreement',
|
name: "Membership Agreement",
|
||||||
description: 'Define membership terms and conditions',
|
description: "Define membership terms and conditions",
|
||||||
available: false,
|
available: false,
|
||||||
url: '#'
|
url: "#",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'patronage',
|
id: "patronage",
|
||||||
name: 'Patronage Policy',
|
name: "Patronage Policy",
|
||||||
description: 'Structure your patronage distribution system',
|
description: "Structure your patronage distribution system",
|
||||||
available: false,
|
available: false,
|
||||||
url: '#'
|
url: "#",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'conflict',
|
id: "conflict",
|
||||||
name: 'Conflict Resolution Process',
|
name: "Conflict Resolution Process",
|
||||||
description: 'Establish clear conflict resolution procedures',
|
description: "Establish clear conflict resolution procedures",
|
||||||
available: false,
|
available: false,
|
||||||
url: '#'
|
url: "#",
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -180,18 +209,6 @@ const pdfDownloads = [
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dither-shadow {
|
|
||||||
background-image: repeating-linear-gradient(
|
|
||||||
45deg,
|
|
||||||
transparent,
|
|
||||||
transparent 2px,
|
|
||||||
currentColor 2px,
|
|
||||||
currentColor 4px
|
|
||||||
);
|
|
||||||
opacity: 0.1;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dither-tag {
|
.dither-tag {
|
||||||
background-image: repeating-linear-gradient(
|
background-image: repeating-linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
|
|
@ -202,4 +219,4 @@ const pdfDownloads = [
|
||||||
);
|
);
|
||||||
background-size: 4px 4px;
|
background-size: 4px 4px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -416,7 +416,7 @@ export const useBudgetStore = defineStore(
|
||||||
return sum + (m.monthlyPayPlanned || 0);
|
return sum + (m.monthlyPayPlanned || 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
let monthlyOncostAmount = monthlyAllocatedPayroll * (oncostPct / 100);
|
let monthlyOncostAmount = Math.round(monthlyAllocatedPayroll * (oncostPct / 100));
|
||||||
|
|
||||||
// Calculate projected balance after this month's expenses and payroll
|
// Calculate projected balance after this month's expenses and payroll
|
||||||
const totalExpensesWithPayroll = nonPayrollExpenses + monthlyAllocatedPayroll + monthlyOncostAmount;
|
const totalExpensesWithPayroll = nonPayrollExpenses + monthlyAllocatedPayroll + monthlyOncostAmount;
|
||||||
|
|
@ -442,7 +442,7 @@ export const useBudgetStore = defineStore(
|
||||||
}
|
}
|
||||||
|
|
||||||
monthlyAllocatedPayroll = maxSustainablePayroll;
|
monthlyAllocatedPayroll = maxSustainablePayroll;
|
||||||
monthlyOncostAmount = monthlyAllocatedPayroll * (oncostPct / 100);
|
monthlyOncostAmount = Math.round(monthlyAllocatedPayroll * (oncostPct / 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update running balance with actual net income after payroll adjustments
|
// Update running balance with actual net income after payroll adjustments
|
||||||
|
|
@ -451,17 +451,17 @@ export const useBudgetStore = defineStore(
|
||||||
|
|
||||||
// Update this specific month's payroll values
|
// Update this specific month's payroll values
|
||||||
if (basePayrollIndex !== -1) {
|
if (basePayrollIndex !== -1) {
|
||||||
budgetWorksheet.value.expenses[basePayrollIndex].monthlyValues[monthKey] = monthlyAllocatedPayroll;
|
budgetWorksheet.value.expenses[basePayrollIndex].monthlyValues[monthKey] = Math.round(monthlyAllocatedPayroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oncostIndex !== -1) {
|
if (oncostIndex !== -1) {
|
||||||
budgetWorksheet.value.expenses[oncostIndex].monthlyValues[monthKey] = monthlyOncostAmount;
|
budgetWorksheet.value.expenses[oncostIndex].monthlyValues[monthKey] = Math.round(monthlyOncostAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle legacy single payroll entry
|
// Handle legacy single payroll entry
|
||||||
if (legacyIndex !== -1 && basePayrollIndex === -1) {
|
if (legacyIndex !== -1 && basePayrollIndex === -1) {
|
||||||
const combinedPayroll = monthlyAllocatedPayroll * (1 + oncostPct / 100);
|
const combinedPayroll = Math.round(monthlyAllocatedPayroll * (1 + oncostPct / 100));
|
||||||
budgetWorksheet.value.expenses[legacyIndex].monthlyValues[monthKey] = combinedPayroll;
|
budgetWorksheet.value.expenses[legacyIndex].monthlyValues[monthKey] = Math.round(combinedPayroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accumulate for annual totals
|
// Accumulate for annual totals
|
||||||
|
|
@ -490,9 +490,9 @@ export const useBudgetStore = defineStore(
|
||||||
// Update annual values based on actual totals
|
// Update annual values based on actual totals
|
||||||
if (basePayrollIndex !== -1) {
|
if (basePayrollIndex !== -1) {
|
||||||
budgetWorksheet.value.expenses[basePayrollIndex].values = {
|
budgetWorksheet.value.expenses[basePayrollIndex].values = {
|
||||||
year1: { best: totalAnnualPayroll * 1.2, worst: totalAnnualPayroll * 0.8, mostLikely: totalAnnualPayroll },
|
year1: { best: Math.round(totalAnnualPayroll * 1.2), worst: Math.round(totalAnnualPayroll * 0.8), mostLikely: Math.round(totalAnnualPayroll) },
|
||||||
year2: { best: totalAnnualPayroll * 1.3, worst: totalAnnualPayroll * 0.9, mostLikely: totalAnnualPayroll * 1.1 },
|
year2: { best: Math.round(totalAnnualPayroll * 1.3), worst: Math.round(totalAnnualPayroll * 0.9), mostLikely: Math.round(totalAnnualPayroll * 1.1) },
|
||||||
year3: { best: totalAnnualPayroll * 1.5, worst: totalAnnualPayroll, mostLikely: totalAnnualPayroll * 1.25 }
|
year3: { best: Math.round(totalAnnualPayroll * 1.5), worst: Math.round(totalAnnualPayroll), mostLikely: Math.round(totalAnnualPayroll * 1.25) }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -501,9 +501,9 @@ export const useBudgetStore = defineStore(
|
||||||
budgetWorksheet.value.expenses[oncostIndex].name = `Payroll Taxes & Benefits (${oncostPct}%)`;
|
budgetWorksheet.value.expenses[oncostIndex].name = `Payroll Taxes & Benefits (${oncostPct}%)`;
|
||||||
|
|
||||||
budgetWorksheet.value.expenses[oncostIndex].values = {
|
budgetWorksheet.value.expenses[oncostIndex].values = {
|
||||||
year1: { best: totalAnnualOncosts * 1.2, worst: totalAnnualOncosts * 0.8, mostLikely: totalAnnualOncosts },
|
year1: { best: Math.round(totalAnnualOncosts * 1.2), worst: Math.round(totalAnnualOncosts * 0.8), mostLikely: Math.round(totalAnnualOncosts) },
|
||||||
year2: { best: totalAnnualOncosts * 1.3, worst: totalAnnualOncosts * 0.9, mostLikely: totalAnnualOncosts * 1.1 },
|
year2: { best: Math.round(totalAnnualOncosts * 1.3), worst: Math.round(totalAnnualOncosts * 0.9), mostLikely: Math.round(totalAnnualOncosts * 1.1) },
|
||||||
year3: { best: totalAnnualOncosts * 1.5, worst: totalAnnualOncosts, mostLikely: totalAnnualOncosts * 1.25 }
|
year3: { best: Math.round(totalAnnualOncosts * 1.5), worst: Math.round(totalAnnualOncosts), mostLikely: Math.round(totalAnnualOncosts * 1.25) }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -511,9 +511,9 @@ export const useBudgetStore = defineStore(
|
||||||
if (legacyIndex !== -1 && basePayrollIndex === -1) {
|
if (legacyIndex !== -1 && basePayrollIndex === -1) {
|
||||||
const totalCombined = totalAnnualPayroll + totalAnnualOncosts;
|
const totalCombined = totalAnnualPayroll + totalAnnualOncosts;
|
||||||
budgetWorksheet.value.expenses[legacyIndex].values = {
|
budgetWorksheet.value.expenses[legacyIndex].values = {
|
||||||
year1: { best: totalCombined * 1.2, worst: totalCombined * 0.8, mostLikely: totalCombined },
|
year1: { best: Math.round(totalCombined * 1.2), worst: Math.round(totalCombined * 0.8), mostLikely: Math.round(totalCombined) },
|
||||||
year2: { best: totalCombined * 1.3, worst: totalCombined * 0.9, mostLikely: totalCombined * 1.1 },
|
year2: { best: Math.round(totalCombined * 1.3), worst: Math.round(totalCombined * 0.9), mostLikely: Math.round(totalCombined * 1.1) },
|
||||||
year3: { best: totalCombined * 1.5, worst: totalCombined, mostLikely: totalCombined * 1.25 }
|
year3: { best: Math.round(totalCombined * 1.5), worst: Math.round(totalCombined), mostLikely: Math.round(totalCombined * 1.25) }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -807,19 +807,19 @@ export const useBudgetStore = defineStore(
|
||||||
monthlyValues,
|
monthlyValues,
|
||||||
values: {
|
values: {
|
||||||
year1: {
|
year1: {
|
||||||
best: annualAmount,
|
best: Math.round(annualAmount),
|
||||||
worst: annualAmount * 0.8,
|
worst: Math.round(annualAmount * 0.8),
|
||||||
mostLikely: annualAmount,
|
mostLikely: Math.round(annualAmount),
|
||||||
},
|
},
|
||||||
year2: {
|
year2: {
|
||||||
best: annualAmount * 1.1,
|
best: Math.round(annualAmount * 1.1),
|
||||||
worst: annualAmount * 0.9,
|
worst: Math.round(annualAmount * 0.9),
|
||||||
mostLikely: annualAmount * 1.05,
|
mostLikely: Math.round(annualAmount * 1.05),
|
||||||
},
|
},
|
||||||
year3: {
|
year3: {
|
||||||
best: annualAmount * 1.2,
|
best: Math.round(annualAmount * 1.2),
|
||||||
worst: annualAmount,
|
worst: Math.round(annualAmount),
|
||||||
mostLikely: annualAmount * 1.1,
|
mostLikely: Math.round(annualAmount * 1.1),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export const useCoopBuilderStore = defineStore("coop", {
|
||||||
payrollOncostPct: 25,
|
payrollOncostPct: 25,
|
||||||
|
|
||||||
// Cash flow management
|
// Cash flow management
|
||||||
minCashThreshold: 5000, // Minimum cash balance to maintain
|
minCashThreshold: 0, // Minimum cash balance to maintain
|
||||||
savingsTargetMonths: 6,
|
savingsTargetMonths: 6,
|
||||||
minCashCushion: 10000,
|
minCashCushion: 10000,
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue