204 lines
6 KiB
Vue
204 lines
6 KiB
Vue
<template>
|
|
<div class="space-y-6">
|
|
<div>
|
|
<h3 class="text-lg font-medium mb-4">Operating Costs</h3>
|
|
<p class="text-gray-600 mb-6">
|
|
Add your monthly overhead costs. Production costs are handled
|
|
separately.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Overhead Costs -->
|
|
<div class="space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<h4 class="font-medium">Monthly Overhead</h4>
|
|
<UButton size="sm" @click="addOverheadCost" icon="i-heroicons-plus">
|
|
Add Cost
|
|
</UButton>
|
|
</div>
|
|
|
|
<div
|
|
v-if="overheadCosts.length === 0"
|
|
class="text-center py-8 text-gray-500">
|
|
<p>No overhead costs added yet.</p>
|
|
<p class="text-sm">
|
|
Add costs like rent, tools, insurance, or other recurring expenses.
|
|
</p>
|
|
</div>
|
|
|
|
<div
|
|
v-for="cost in overheadCosts"
|
|
:key="cost.id"
|
|
class="p-4 border border-gray-200 rounded-lg">
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<UFormField label="Cost Name" required>
|
|
<UInput
|
|
v-model="cost.name"
|
|
placeholder="Office rent"
|
|
@update:model-value="saveCost(cost)"
|
|
@blur="saveCost(cost)" />
|
|
</UFormField>
|
|
|
|
<UFormField label="Monthly Amount" required>
|
|
<UInput
|
|
v-model.number="cost.amount"
|
|
type="number"
|
|
min="0"
|
|
step="0.01"
|
|
placeholder="800.00"
|
|
@update:model-value="saveCost(cost)"
|
|
@blur="saveCost(cost)">
|
|
<template #leading>
|
|
<span class="text-gray-500">€</span>
|
|
</template>
|
|
</UInput>
|
|
</UFormField>
|
|
|
|
<UFormField label="Category">
|
|
<USelect
|
|
v-model="cost.category"
|
|
:items="categoryOptions"
|
|
@update:model-value="saveCost(cost)" />
|
|
</UFormField>
|
|
</div>
|
|
|
|
<div class="flex justify-end mt-4 pt-4 border-t border-gray-100">
|
|
<UButton
|
|
size="sm"
|
|
variant="ghost"
|
|
color="red"
|
|
@click="removeCost(cost.id)">
|
|
Remove
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cost Categories -->
|
|
<div class="bg-blue-50 rounded-lg p-4">
|
|
<h4 class="font-medium text-sm mb-2 text-blue-900">Cost Categories</h4>
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
|
|
<div>
|
|
<span class="font-medium text-blue-800">Operations:</span>
|
|
<span class="text-blue-700 ml-1">Rent, utilities, insurance</span>
|
|
</div>
|
|
<div>
|
|
<span class="font-medium text-blue-800">Tools:</span>
|
|
<span class="text-blue-700 ml-1">Software, hardware, licenses</span>
|
|
</div>
|
|
<div>
|
|
<span class="font-medium text-blue-800">Professional:</span>
|
|
<span class="text-blue-700 ml-1">Legal, accounting, consulting</span>
|
|
</div>
|
|
<div>
|
|
<span class="font-medium text-blue-800">Other:</span>
|
|
<span class="text-blue-700 ml-1">Miscellaneous costs</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary -->
|
|
<div class="bg-gray-50 rounded-lg p-4">
|
|
<h4 class="font-medium text-sm mb-2">Cost Summary</h4>
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<span class="text-gray-600">Total items:</span>
|
|
<span class="font-medium ml-1">{{ overheadCosts.length }}</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-gray-600">Monthly overhead:</span>
|
|
<span class="font-medium ml-1">€{{ totalMonthlyCosts }}</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-gray-600">Largest cost:</span>
|
|
<span class="font-medium ml-1">€{{ largestCost }}</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-gray-600">Avg per item:</span>
|
|
<span class="font-medium ml-1">€{{ avgCostPerItem }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useDebounceFn } from "@vueuse/core";
|
|
import { storeToRefs } from "pinia";
|
|
const emit = defineEmits<{
|
|
"save-status": [status: "saving" | "saved" | "error"];
|
|
}>();
|
|
|
|
// Store
|
|
const budgetStore = useBudgetStore();
|
|
const { overheadCosts } = storeToRefs(budgetStore);
|
|
|
|
// Category options
|
|
const categoryOptions = [
|
|
{ label: "Operations", value: "Operations" },
|
|
{ label: "Tools & Software", value: "Tools" },
|
|
{ label: "Professional Services", value: "Professional" },
|
|
{ label: "Marketing", value: "Marketing" },
|
|
{ label: "Other", value: "Other" },
|
|
];
|
|
|
|
// Computeds
|
|
const totalMonthlyCosts = computed(() =>
|
|
overheadCosts.value.reduce((sum, cost) => sum + (cost.amount || 0), 0)
|
|
);
|
|
|
|
const largestCost = computed(() => {
|
|
if (overheadCosts.value.length === 0) return 0;
|
|
return Math.max(...overheadCosts.value.map((c) => c.amount || 0));
|
|
});
|
|
|
|
const avgCostPerItem = computed(() => {
|
|
if (overheadCosts.value.length === 0) return 0;
|
|
return Math.round(totalMonthlyCosts.value / overheadCosts.value.length);
|
|
});
|
|
|
|
// Live-write with debounce
|
|
const debouncedSave = useDebounceFn((cost: any) => {
|
|
emit("save-status", "saving");
|
|
|
|
try {
|
|
// Find and update existing cost
|
|
const existingCost = overheadCosts.value.find((c) => c.id === cost.id);
|
|
if (existingCost) {
|
|
// Store will handle reactivity through the ref
|
|
Object.assign(existingCost, cost);
|
|
}
|
|
|
|
emit("save-status", "saved");
|
|
} catch (error) {
|
|
console.error("Failed to save cost:", error);
|
|
emit("save-status", "error");
|
|
}
|
|
}, 300);
|
|
|
|
function saveCost(cost: any) {
|
|
if (cost.name && cost.amount >= 0) {
|
|
debouncedSave(cost);
|
|
}
|
|
}
|
|
|
|
function addOverheadCost() {
|
|
const newCost = {
|
|
id: Date.now().toString(),
|
|
name: "",
|
|
amount: 0,
|
|
category: "Operations",
|
|
recurring: true,
|
|
};
|
|
|
|
budgetStore.addOverheadLine({
|
|
name: newCost.name,
|
|
amountMonthly: newCost.amount,
|
|
category: newCost.category,
|
|
});
|
|
}
|
|
|
|
function removeCost(id: string) {
|
|
budgetStore.removeOverheadLine(id);
|
|
}
|
|
</script>
|