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 { error } from '@sveltejs/kit';
|
||||||
import type { LayoutServerLoad } from './$types';
|
import type { LayoutServerLoad } from './$types';
|
||||||
import { db } from '$lib/server/db/index.js';
|
import { db } from '$lib/server/db/index.js';
|
||||||
import { companies } from '$lib/server/db/schema.js';
|
import { companies, companyAccounts, companyAccountTransactions } from '$lib/server/db/schema.js';
|
||||||
import { eq, and, isNull } from 'drizzle-orm';
|
import { eq, and, isNull, sql } from 'drizzle-orm';
|
||||||
import { requireAuth, getCompanyRoles } from '$lib/server/authorization.js';
|
import { requireAuth, getCompanyRoles } from '$lib/server/authorization.js';
|
||||||
import type { CompanyRole } from '$lib/types/index.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');
|
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 {
|
return {
|
||||||
company: {
|
company: {
|
||||||
id: company.id,
|
id: company.id,
|
||||||
name: company.name,
|
name: company.name,
|
||||||
description: company.description,
|
description: company.description,
|
||||||
totalBudget: company.totalBudget,
|
totalBudget: balanceRow?.total ?? '0',
|
||||||
currency: company.currency
|
currency: company.currency
|
||||||
},
|
},
|
||||||
companyRoles: roles
|
companyRoles: roles
|
||||||
|
|||||||
@@ -69,43 +69,6 @@ export const load: PageServerLoad = async ({ parent, params }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
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 }) => {
|
allocate: async ({ request, locals, params }) => {
|
||||||
const { user } = await requireCompanyRole(locals, params.companyId, 'manager');
|
const { user } = await requireCompanyRole(locals, params.companyId, 'manager');
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
const canAllocate = $derived(data.companyRoles.includes('admin') || data.companyRoles.includes('manager'));
|
const canAllocate = $derived(data.companyRoles.includes('admin') || data.companyRoles.includes('manager'));
|
||||||
const isAdmin = $derived(data.companyRoles.includes('admin'));
|
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) {
|
function getEventStyle(event: string) {
|
||||||
const styles: Record<string, { icon: string; bg: string; text: string; badge: string; label: string }> = {
|
const styles: Record<string, { icon: string; bg: string; text: string; badge: string; label: string }> = {
|
||||||
@@ -44,65 +44,17 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div>
|
<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>
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Budget Allocation</h2>
|
||||||
{#if isAdmin}
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<button
|
Total budget reflects the sum of your account balances. Manage funds via the Accounts tab.
|
||||||
onclick={() => (showAddBudget = !showAddBudget)}
|
</p>
|
||||||
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}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if form?.error}
|
{#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>
|
<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}
|
{/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 -->
|
<!-- Summary -->
|
||||||
<div class="mb-6 grid gap-4 sm:grid-cols-4">
|
<div class="mb-6 grid gap-4 sm:grid-cols-4">
|
||||||
<!-- Remaining — hero card -->
|
<!-- Remaining — hero card -->
|
||||||
|
|||||||
Reference in New Issue
Block a user