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 {
|
||||
@apply bg-black dark:bg-neutral-600;
|
||||
@apply bg-black dark:bg-neutral-400;
|
||||
background-image: radial-gradient(white 1px, transparent 1px);
|
||||
background-size: 2px 2px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,114 +2,164 @@
|
|||
<div class="space-y-8">
|
||||
<!-- Annual Budget Overview -->
|
||||
<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">
|
||||
<table class="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr class="border-b-2 border-black bg-neutral-100">
|
||||
<th class="border-r-1 border-black px-4 py-3 text-left font-bold">
|
||||
Category
|
||||
</th>
|
||||
<th
|
||||
class="border-r border-neutral-400 px-4 py-3 text-right font-bold">
|
||||
Planned
|
||||
</th>
|
||||
<th class="px-4 py-3 text-right font-bold">%</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Revenue Section -->
|
||||
<tr class="bg-black text-white">
|
||||
<td class="px-4 py-2 font-bold" colspan="3">REVENUE</td>
|
||||
</tr>
|
||||
<!-- Revenue Categories -->
|
||||
<tr
|
||||
v-for="(category, index) in revenueCategories"
|
||||
:key="`rev-${index}`"
|
||||
class="border-t border-neutral-200 dark:border-neutral-700 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>
|
||||
|
||||
<!-- Revenue Categories -->
|
||||
<tr
|
||||
v-for="(category, index) in revenueCategories"
|
||||
:key="`rev-${index}`"
|
||||
class="border-t border-neutral-200"
|
||||
v-show="category.planned > 0">
|
||||
<td class="border-r-1 border-black px-4 py-2">
|
||||
{{ category.name }}
|
||||
</td>
|
||||
<td class="border-r border-neutral-400 px-4 py-2 text-right">
|
||||
{{ formatCurrency(category.planned) }}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-right">{{ category.percentage }}%</td>
|
||||
</tr>
|
||||
<!-- Total Revenue -->
|
||||
<tr
|
||||
class="border-t-2 border-black dark:border-neutral-400 font-semibold bg-neutral-50 dark:bg-neutral-800">
|
||||
<td
|
||||
class="border-r-1 border-black dark:border-neutral-400 px-4 py-2 text-black dark:text-white">
|
||||
Total Revenue
|
||||
</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(totalRevenuePlanned) }}
|
||||
</td>
|
||||
<td
|
||||
class="px-4 py-2 text-right text-black dark:text-white font-mono">
|
||||
100%
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Total Revenue -->
|
||||
<tr class="border-t-2 border-black font-semibold bg-neutral-50">
|
||||
<td class="border-r-1 border-black px-4 py-2">Total Revenue</td>
|
||||
<td class="border-r border-neutral-400 px-4 py-2 text-right">
|
||||
{{ formatCurrency(totalRevenuePlanned) }}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-right">100%</td>
|
||||
</tr>
|
||||
<!-- Revenue Diversification Guidance -->
|
||||
<tr class="bg-neutral-50 dark:bg-neutral-800">
|
||||
<td
|
||||
colspan="3"
|
||||
class="border-t border-neutral-300 dark:border-neutral-700 px-4 py-3 text-black dark:text-white">
|
||||
<div class="text-sm">
|
||||
<p class="font-medium mb-2 text-black dark:text-white">
|
||||
{{ 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 -->
|
||||
<tr :class="guidanceBackgroundClass">
|
||||
<td colspan="3" class="border-t border-neutral-300 px-4 py-3">
|
||||
<div class="text-sm">
|
||||
<p class="font-medium mb-2">{{ diversificationGuidance }}</p>
|
||||
<p
|
||||
class="text-neutral-600 mb-2"
|
||||
v-if="suggestedCategories.length > 0">
|
||||
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 -->
|
||||
<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">
|
||||
EXPENSES
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Expenses Section -->
|
||||
<tr class="bg-black text-white">
|
||||
<td class="px-4 py-2 font-bold" colspan="3">EXPENSES</td>
|
||||
</tr>
|
||||
<!-- Expense Categories -->
|
||||
<tr
|
||||
v-for="(category, index) in expenseCategories"
|
||||
: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 -->
|
||||
<tr
|
||||
v-for="(category, index) in expenseCategories"
|
||||
:key="`exp-${index}`"
|
||||
class="border-t border-neutral-200"
|
||||
v-show="category.planned > 0">
|
||||
<td class="border-r-1 border-black px-4 py-2">
|
||||
{{ category.name }}
|
||||
</td>
|
||||
<td class="border-r border-neutral-400 px-4 py-2 text-right">
|
||||
{{ formatCurrency(category.planned) }}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-right">{{ category.percentage }}%</td>
|
||||
</tr>
|
||||
<!-- Total Expenses -->
|
||||
<tr class="font-semibold bg-neutral-50 dark:bg-neutral-800">
|
||||
<td
|
||||
class="border-r-1 border-black dark:border-neutral-400 px-4 py-2 text-black dark:text-white">
|
||||
Total Expenses
|
||||
</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(totalExpensesPlanned) }}
|
||||
</td>
|
||||
<td
|
||||
class="px-4 py-2 text-right text-black dark:text-white font-mono">
|
||||
100%
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Total Expenses -->
|
||||
<tr class="border-t-2 border-black font-semibold bg-neutral-50">
|
||||
<td class="border-r-1 border-black px-4 py-2">Total Expenses</td>
|
||||
<td class="border-r border-neutral-400 px-4 py-2 text-right">
|
||||
{{ formatCurrency(totalExpensesPlanned) }}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-right">100%</td>
|
||||
</tr>
|
||||
|
||||
<!-- Net Total -->
|
||||
<tr
|
||||
class="border-t-2 border-black font-bold text-lg"
|
||||
:class="netTotalClass">
|
||||
<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">
|
||||
{{ formatCurrency(netTotal) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">-</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Net Total -->
|
||||
<tr
|
||||
class="border-t-2 border-black dark:border-neutral-400 font-bold text-lg"
|
||||
:class="netTotalClass">
|
||||
<td
|
||||
class="border-r-1 border-black dark:border-neutral-400 px-4 py-3 text-black dark:text-white">
|
||||
NET TOTAL
|
||||
</td>
|
||||
<td
|
||||
class="border-r border-neutral-400 dark:border-neutral-600 px-4 py-3 text-right text-black dark:text-white font-mono">
|
||||
{{ formatCurrency(netTotal) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-black dark:text-white">
|
||||
-
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -242,9 +292,9 @@ const netTotal = computed(
|
|||
);
|
||||
|
||||
const netTotalClass = computed(() => {
|
||||
if (netTotal.value > 0) return "bg-green-50";
|
||||
if (netTotal.value < 0) return "bg-red-50";
|
||||
return "bg-neutral-50";
|
||||
if (netTotal.value > 0) return "bg-green-50 dark:bg-green-950";
|
||||
if (netTotal.value < 0) return "bg-red-50 dark:bg-red-950";
|
||||
return "bg-neutral-50 dark:bg-neutral-800";
|
||||
});
|
||||
|
||||
// Diversification guidance
|
||||
|
|
@ -308,28 +358,6 @@ const diversificationGuidance = computed(() => {
|
|||
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
|
||||
const suggestedCategories = computed(() => {
|
||||
const categoriesWithRevenue = revenueCategories.value.filter(
|
||||
|
|
@ -394,10 +422,11 @@ function formatCurrency(amount: number): string {
|
|||
}
|
||||
|
||||
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-neutral-500";
|
||||
if (percentage > 50) return "text-red-600 dark:text-red-400 font-bold";
|
||||
if (percentage > 35)
|
||||
return "text-yellow-600 dark:text-yellow-400 font-semibold";
|
||||
if (percentage > 20) return "text-black dark:text-white font-medium";
|
||||
return "text-neutral-500 dark:text-neutral-400";
|
||||
}
|
||||
|
||||
// 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 => {
|
||||
// Reduce service revenue by 30%
|
||||
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
|
||||
})
|
||||
|
|
@ -128,7 +128,7 @@ export function useCoopBuilder() {
|
|||
if (revenueDelay > 0) {
|
||||
adjustedStreams = adjustedStreams.map(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
|
||||
const payrollCost = monthlyPayroll(scenarioMembers || [], currentMode) || 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
|
||||
const totalRevenue = (scenarioStreams || []).reduce((sum, s) => sum + (s.monthly || 0), 0)
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export function useCushionForecast() {
|
|||
const operatingMode = policiesStore.operatingMode || 'minimum'
|
||||
const payrollCost = monthlyPayroll(membersStore.members, operatingMode)
|
||||
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)
|
||||
|
||||
return totalPayroll + overheadCost
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const useDeferredMetrics = () => {
|
|||
): number => {
|
||||
const totalTargetHours = members.reduce((sum, member) => sum + (member.targetHours || 0), 0)
|
||||
const grossPayroll = totalTargetHours * hourlyWage
|
||||
return grossPayroll * (1 + oncostPct / 100)
|
||||
return Math.round(grossPayroll * (1 + oncostPct / 100))
|
||||
}
|
||||
|
||||
const calculateDeferredRatio = (
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export function usePayrollAllocation() {
|
|||
|
||||
// Total payroll with oncosts
|
||||
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
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export const useRunway = () => {
|
|||
|
||||
// Add oncosts
|
||||
const oncostPct = policiesStore.payrollOncostPct || 0
|
||||
const totalPayroll = payrollCost * (1 + oncostPct / 100)
|
||||
const totalPayroll = Math.round(payrollCost * (1 + oncostPct / 100))
|
||||
|
||||
// Add overhead costs
|
||||
const overheadCost = budgetStore.overheadCosts.reduce((sum, cost) => sum + (cost.amount || 0), 0)
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export const useStoreSync = () => {
|
|||
id: member.id,
|
||||
name: member.displayName,
|
||||
role: member.role,
|
||||
hoursPerMonth: Number(((member.hoursPerWeek || 0) * 4.33).toFixed(2)),
|
||||
hoursPerMonth: Math.round((member.hoursPerWeek || 0) * 4.33),
|
||||
minMonthlyNeeds: member.minMonthlyNeeds,
|
||||
monthlyPayPlanned: member.monthlyPayPlanned,
|
||||
targetMonthlyPay: member.targetMonthlyPay,
|
||||
|
|
|
|||
1301
pages/budget.vue
1301
pages/budget.vue
File diff suppressed because it is too large
Load diff
|
|
@ -1,46 +1,56 @@
|
|||
<template>
|
||||
<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="mb-8">
|
||||
<h1 class="text-3xl font-bold text-neutral-900 dark:text-white mb-2">
|
||||
More Resources & Templates
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="space-y-8">
|
||||
<!-- External Templates 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
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Miro Template -->
|
||||
<div class="template-card">
|
||||
<div 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"
|
||||
>
|
||||
<h3 class="text-xl font-semibold text-neutral-900 dark:text-white mb-2">
|
||||
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">
|
||||
<h3
|
||||
class="text-xl font-semibold text-neutral-900 dark:text-white mb-2">
|
||||
Goals & Values Exercise
|
||||
</h3>
|
||||
<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>
|
||||
<a
|
||||
href="https://miro.com/miroverse/goals-values-exercise/"
|
||||
target="_blank"
|
||||
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
|
||||
<svg 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
|
||||
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>
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -48,25 +58,34 @@
|
|||
|
||||
<!-- CommunityRule Templates -->
|
||||
<div class="template-card">
|
||||
<div 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"
|
||||
>
|
||||
<h3 class="text-xl font-semibold text-neutral-900 dark:text-white mb-2">
|
||||
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">
|
||||
<h3
|
||||
class="text-xl font-semibold text-neutral-900 dark:text-white mb-2">
|
||||
CommunityRule Governance Templates
|
||||
</h3>
|
||||
<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>
|
||||
<a
|
||||
href="https://communityrule.info/"
|
||||
target="_blank"
|
||||
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
|
||||
<svg 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
|
||||
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>
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -76,7 +95,8 @@
|
|||
|
||||
<!-- PDF Downloads 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
|
||||
</h2>
|
||||
<p class="text-neutral-700 dark:text-neutral-200 mb-4">
|
||||
|
|
@ -86,21 +106,23 @@
|
|||
<div
|
||||
v-for="pdf in pdfDownloads"
|
||||
:key="pdf.id"
|
||||
class="template-card"
|
||||
>
|
||||
<div class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
||||
class="template-card">
|
||||
<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">
|
||||
<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 }}
|
||||
</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
|
||||
</span>
|
||||
</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 }}
|
||||
</p>
|
||||
<button
|
||||
|
|
@ -109,13 +131,20 @@
|
|||
'inline-flex items-center px-4 py-2 font-medium transition-opacity',
|
||||
pdf.available
|
||||
? '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'
|
||||
]"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" 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" />
|
||||
: 'bg-neutral-200 dark:bg-neutral-800 text-neutral-500 dark:text-neutral-500 cursor-not-allowed',
|
||||
]">
|
||||
<svg
|
||||
class="w-4 h-4 mr-2"
|
||||
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>
|
||||
{{ pdf.available ? 'Download' : 'Coming Soon' }}
|
||||
{{ pdf.available ? "Download" : "Coming Soon" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -131,48 +160,48 @@
|
|||
// PDF downloads list with placeholder data
|
||||
const pdfDownloads = [
|
||||
{
|
||||
id: 'bylaws',
|
||||
name: 'Bylaws Wizard',
|
||||
description: 'Create comprehensive bylaws for your cooperative',
|
||||
id: "bylaws",
|
||||
name: "Bylaws Wizard",
|
||||
description: "Create comprehensive bylaws for your cooperative",
|
||||
available: false,
|
||||
url: '#'
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
id: 'operating-agreement',
|
||||
name: 'Operating Agreement Wizard',
|
||||
description: 'Draft an operating agreement for your LLC cooperative',
|
||||
id: "operating-agreement",
|
||||
name: "Operating Agreement Wizard",
|
||||
description: "Draft an operating agreement for your LLC cooperative",
|
||||
available: false,
|
||||
url: '#'
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
id: 'articles',
|
||||
name: 'Articles of Incorporation',
|
||||
description: 'Template for articles of incorporation',
|
||||
id: "articles",
|
||||
name: "Articles of Incorporation",
|
||||
description: "Template for articles of incorporation",
|
||||
available: false,
|
||||
url: '#'
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
id: 'membership',
|
||||
name: 'Membership Agreement',
|
||||
description: 'Define membership terms and conditions',
|
||||
id: "membership",
|
||||
name: "Membership Agreement",
|
||||
description: "Define membership terms and conditions",
|
||||
available: false,
|
||||
url: '#'
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
id: 'patronage',
|
||||
name: 'Patronage Policy',
|
||||
description: 'Structure your patronage distribution system',
|
||||
id: "patronage",
|
||||
name: "Patronage Policy",
|
||||
description: "Structure your patronage distribution system",
|
||||
available: false,
|
||||
url: '#'
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
id: 'conflict',
|
||||
name: 'Conflict Resolution Process',
|
||||
description: 'Establish clear conflict resolution procedures',
|
||||
id: "conflict",
|
||||
name: "Conflict Resolution Process",
|
||||
description: "Establish clear conflict resolution procedures",
|
||||
available: false,
|
||||
url: '#'
|
||||
}
|
||||
]
|
||||
url: "#",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -180,18 +209,6 @@ const pdfDownloads = [
|
|||
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 {
|
||||
background-image: repeating-linear-gradient(
|
||||
135deg,
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ export const useBudgetStore = defineStore(
|
|||
return sum + (m.monthlyPayPlanned || 0);
|
||||
}, 0);
|
||||
|
||||
let monthlyOncostAmount = monthlyAllocatedPayroll * (oncostPct / 100);
|
||||
let monthlyOncostAmount = Math.round(monthlyAllocatedPayroll * (oncostPct / 100));
|
||||
|
||||
// Calculate projected balance after this month's expenses and payroll
|
||||
const totalExpensesWithPayroll = nonPayrollExpenses + monthlyAllocatedPayroll + monthlyOncostAmount;
|
||||
|
|
@ -442,7 +442,7 @@ export const useBudgetStore = defineStore(
|
|||
}
|
||||
|
||||
monthlyAllocatedPayroll = maxSustainablePayroll;
|
||||
monthlyOncostAmount = monthlyAllocatedPayroll * (oncostPct / 100);
|
||||
monthlyOncostAmount = Math.round(monthlyAllocatedPayroll * (oncostPct / 100));
|
||||
}
|
||||
|
||||
// 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
|
||||
if (basePayrollIndex !== -1) {
|
||||
budgetWorksheet.value.expenses[basePayrollIndex].monthlyValues[monthKey] = monthlyAllocatedPayroll;
|
||||
budgetWorksheet.value.expenses[basePayrollIndex].monthlyValues[monthKey] = Math.round(monthlyAllocatedPayroll);
|
||||
}
|
||||
|
||||
if (oncostIndex !== -1) {
|
||||
budgetWorksheet.value.expenses[oncostIndex].monthlyValues[monthKey] = monthlyOncostAmount;
|
||||
budgetWorksheet.value.expenses[oncostIndex].monthlyValues[monthKey] = Math.round(monthlyOncostAmount);
|
||||
}
|
||||
|
||||
// Handle legacy single payroll entry
|
||||
if (legacyIndex !== -1 && basePayrollIndex === -1) {
|
||||
const combinedPayroll = monthlyAllocatedPayroll * (1 + oncostPct / 100);
|
||||
budgetWorksheet.value.expenses[legacyIndex].monthlyValues[monthKey] = combinedPayroll;
|
||||
const combinedPayroll = Math.round(monthlyAllocatedPayroll * (1 + oncostPct / 100));
|
||||
budgetWorksheet.value.expenses[legacyIndex].monthlyValues[monthKey] = Math.round(combinedPayroll);
|
||||
}
|
||||
|
||||
// Accumulate for annual totals
|
||||
|
|
@ -490,9 +490,9 @@ export const useBudgetStore = defineStore(
|
|||
// Update annual values based on actual totals
|
||||
if (basePayrollIndex !== -1) {
|
||||
budgetWorksheet.value.expenses[basePayrollIndex].values = {
|
||||
year1: { best: totalAnnualPayroll * 1.2, worst: totalAnnualPayroll * 0.8, mostLikely: totalAnnualPayroll },
|
||||
year2: { best: totalAnnualPayroll * 1.3, worst: totalAnnualPayroll * 0.9, mostLikely: totalAnnualPayroll * 1.1 },
|
||||
year3: { best: totalAnnualPayroll * 1.5, worst: totalAnnualPayroll, mostLikely: totalAnnualPayroll * 1.25 }
|
||||
year1: { best: Math.round(totalAnnualPayroll * 1.2), worst: Math.round(totalAnnualPayroll * 0.8), mostLikely: Math.round(totalAnnualPayroll) },
|
||||
year2: { best: Math.round(totalAnnualPayroll * 1.3), worst: Math.round(totalAnnualPayroll * 0.9), mostLikely: Math.round(totalAnnualPayroll * 1.1) },
|
||||
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].values = {
|
||||
year1: { best: totalAnnualOncosts * 1.2, worst: totalAnnualOncosts * 0.8, mostLikely: totalAnnualOncosts },
|
||||
year2: { best: totalAnnualOncosts * 1.3, worst: totalAnnualOncosts * 0.9, mostLikely: totalAnnualOncosts * 1.1 },
|
||||
year3: { best: totalAnnualOncosts * 1.5, worst: totalAnnualOncosts, mostLikely: totalAnnualOncosts * 1.25 }
|
||||
year1: { best: Math.round(totalAnnualOncosts * 1.2), worst: Math.round(totalAnnualOncosts * 0.8), mostLikely: Math.round(totalAnnualOncosts) },
|
||||
year2: { best: Math.round(totalAnnualOncosts * 1.3), worst: Math.round(totalAnnualOncosts * 0.9), mostLikely: Math.round(totalAnnualOncosts * 1.1) },
|
||||
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) {
|
||||
const totalCombined = totalAnnualPayroll + totalAnnualOncosts;
|
||||
budgetWorksheet.value.expenses[legacyIndex].values = {
|
||||
year1: { best: totalCombined * 1.2, worst: totalCombined * 0.8, mostLikely: totalCombined },
|
||||
year2: { best: totalCombined * 1.3, worst: totalCombined * 0.9, mostLikely: totalCombined * 1.1 },
|
||||
year3: { best: totalCombined * 1.5, worst: totalCombined, mostLikely: totalCombined * 1.25 }
|
||||
year1: { best: Math.round(totalCombined * 1.2), worst: Math.round(totalCombined * 0.8), mostLikely: Math.round(totalCombined) },
|
||||
year2: { best: Math.round(totalCombined * 1.3), worst: Math.round(totalCombined * 0.9), mostLikely: Math.round(totalCombined * 1.1) },
|
||||
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,
|
||||
values: {
|
||||
year1: {
|
||||
best: annualAmount,
|
||||
worst: annualAmount * 0.8,
|
||||
mostLikely: annualAmount,
|
||||
best: Math.round(annualAmount),
|
||||
worst: Math.round(annualAmount * 0.8),
|
||||
mostLikely: Math.round(annualAmount),
|
||||
},
|
||||
year2: {
|
||||
best: annualAmount * 1.1,
|
||||
worst: annualAmount * 0.9,
|
||||
mostLikely: annualAmount * 1.05,
|
||||
best: Math.round(annualAmount * 1.1),
|
||||
worst: Math.round(annualAmount * 0.9),
|
||||
mostLikely: Math.round(annualAmount * 1.05),
|
||||
},
|
||||
year3: {
|
||||
best: annualAmount * 1.2,
|
||||
worst: annualAmount,
|
||||
mostLikely: annualAmount * 1.1,
|
||||
best: Math.round(annualAmount * 1.2),
|
||||
worst: Math.round(annualAmount),
|
||||
mostLikely: Math.round(annualAmount * 1.1),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export const useCoopBuilderStore = defineStore("coop", {
|
|||
payrollOncostPct: 25,
|
||||
|
||||
// Cash flow management
|
||||
minCashThreshold: 5000, // Minimum cash balance to maintain
|
||||
minCashThreshold: 0, // Minimum cash balance to maintain
|
||||
savingsTargetMonths: 6,
|
||||
minCashCushion: 10000,
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue