Derive total budget from account balances instead of manual field
Deploy to LXC / deploy (push) Successful in 1m55s
Validate / validate (push) Successful in 38s

Total budget is now sum(account transaction amounts) across all
non-deleted accounts. Removed the manual 'Add Budget' action and form.
Budget page is now read-only for the total; allocations still work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 16:16:05 +07:00
parent 283f0d4dd1
commit bc0699a992
3 changed files with 22 additions and 93 deletions
@@ -1,8 +1,8 @@
import { error } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
import { db } from '$lib/server/db/index.js';
import { companies } from '$lib/server/db/schema.js';
import { eq, and, isNull } from 'drizzle-orm';
import { companies, companyAccounts, companyAccountTransactions } from '$lib/server/db/schema.js';
import { eq, and, isNull, sql } from 'drizzle-orm';
import { requireAuth, getCompanyRoles } from '$lib/server/authorization.js';
import type { CompanyRole } from '$lib/types/index.js';
@@ -27,12 +27,26 @@ export const load: LayoutServerLoad = async ({ locals, params }) => {
error(403, 'Not a member of this company');
}
// Total budget = sum of all non-deleted account balances
const [balanceRow] = await db
.select({
total: sql<string>`coalesce(sum(${companyAccountTransactions.amount}), '0')::text`
})
.from(companyAccountTransactions)
.innerJoin(companyAccounts, eq(companyAccountTransactions.accountId, companyAccounts.id))
.where(
and(
eq(companyAccountTransactions.companyId, company.id),
isNull(companyAccounts.deletedAt)
)
);
return {
company: {
id: company.id,
name: company.name,
description: company.description,
totalBudget: company.totalBudget,
totalBudget: balanceRow?.total ?? '0',
currency: company.currency
},
companyRoles: roles
@@ -69,43 +69,6 @@ export const load: PageServerLoad = async ({ parent, params }) => {
};
export const actions: Actions = {
addBudget: async ({ request, locals, params }) => {
const { user } = await requireCompanyRole(locals, params.companyId, 'admin');
const formData = await request.formData();
const amount = parseFloat(formData.get('amount')?.toString() || '0');
if (isNaN(amount) || amount <= 0) {
return fail(400, { error: 'Amount must be a positive number' });
}
// Get current budget for the log
const [company] = await db
.select({ totalBudget: companies.totalBudget, currency: companies.currency })
.from(companies)
.where(eq(companies.id, params.companyId))
.limit(1);
await db
.update(companies)
.set({
totalBudget: sql`${companies.totalBudget}::numeric + ${amount.toFixed(2)}::numeric`,
updatedAt: new Date()
})
.where(eq(companies.id, params.companyId));
const newTotal = parseFloat(company.totalBudget) + amount;
await logCompanyEvent(
params.companyId,
user.id,
'budget_added',
`Budget increased by ${formatCurrency(amount, company.currency)} (new total: ${formatCurrency(newTotal, company.currency)})`,
{ amount: amount.toFixed(2), previousTotal: company.totalBudget, newTotal: newTotal.toFixed(2) }
);
return { success: true };
},
allocate: async ({ request, locals, params }) => {
const { user } = await requireCompanyRole(locals, params.companyId, 'manager');
@@ -14,7 +14,7 @@
const canAllocate = $derived(data.companyRoles.includes('admin') || data.companyRoles.includes('manager'));
const isAdmin = $derived(data.companyRoles.includes('admin'));
let showAddBudget = $state(false);
// Budget total now comes from account balances — no manual add
function getEventStyle(event: string) {
const styles: Record<string, { icon: string; bg: string; text: string; badge: string; label: string }> = {
@@ -44,65 +44,17 @@
</svelte:head>
<div>
<div class="mb-4 flex items-center justify-between">
<div class="mb-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Budget Allocation</h2>
{#if isAdmin}
<button
onclick={() => (showAddBudget = !showAddBudget)}
class="flex items-center gap-1 rounded-md bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Add Budget
</button>
{/if}
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Total budget reflects the sum of your account balances. Manage funds via the Accounts tab.
</p>
</div>
{#if form?.error}
<div class="mb-4 rounded-md bg-red-50 dark:bg-red-900/30 p-3 text-sm text-red-700 dark:text-red-300">{form.error}</div>
{/if}
<!-- Add Budget form (admin only) -->
{#if showAddBudget && isAdmin}
<div class="mb-6 rounded-lg border-2 border-green-200 dark:border-green-700 bg-green-50 dark:bg-green-900/30 p-5">
<h3 class="mb-3 font-medium text-gray-900 dark:text-white">Replenish Company Budget</h3>
<form method="POST" action="?/addBudget" use:enhance={() => {
return async ({ update }) => {
await update();
showAddBudget = false;
};
}} class="flex items-end gap-3">
<div class="flex-1">
<label for="addAmount" class="mb-1 block text-sm text-gray-700 dark:text-gray-300">Amount to Add ({currency})</label>
<input
type="number"
id="addAmount"
name="amount"
step="0.01"
min="0.01"
required
placeholder="e.g. 100000"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm focus:border-green-500 focus:ring-1 focus:ring-green-500 focus:outline-none"
/>
</div>
<button
type="submit"
class="rounded-md bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700"
>
Add to Budget
</button>
<button
type="button"
onclick={() => (showAddBudget = false)}
class="rounded-md px-4 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
>
Cancel
</button>
</form>
</div>
{/if}
<!-- Summary -->
<div class="mb-6 grid gap-4 sm:grid-cols-4">
<!-- Remaining — hero card -->