Derive total budget from account balances instead of manual field
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:
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user