Add confirmed-sales income to budget and overview

Budget page load now computes per-project income (net of withholding)
from confirmed sales. Overview has a full-width Income KPI showing
total confirmed net revenue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 12:48:06 +07:00
parent 1c15cbc36e
commit 0795d78bdf
3 changed files with 72 additions and 5 deletions
@@ -1,6 +1,6 @@
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/db/index.js';
import { projects, expenses } from '$lib/server/db/schema.js';
import { projects, expenses, sales, saleLineItems } from '$lib/server/db/schema.js';
import { eq, and, sql } from 'drizzle-orm';
export const load: PageServerLoad = async ({ parent }) => {
@@ -38,5 +38,20 @@ export const load: PageServerLoad = async ({ parent }) => {
.orderBy(sql`${expenses.createdAt} desc`)
.limit(10);
return { projects: projectList, recentExpenses };
// Total confirmed sales income (net of withholding)
const [incomeRow] = await db
.select({
total: sql<string>`coalesce(sum(
(select sum(${saleLineItems.quantity} * ${saleLineItems.unitPrice} * (1 + ${saleLineItems.taxRate})) from sale_line_items where sale_id = ${sales.id})
* (1 - ${sales.withholdingTaxRate})
), '0')::text`
})
.from(sales)
.where(and(eq(sales.companyId, company.id), eq(sales.status, 'confirmed')));
return {
projects: projectList,
recentExpenses,
totalIncome: incomeRow?.total ?? '0'
};
};
@@ -8,6 +8,7 @@
const allocated = $derived(data.projects.reduce((s, p) => s + parseFloat(p.allocatedBudget), 0));
const spent = $derived(data.projects.reduce((s, p) => s + parseFloat(p.spent), 0));
const total = $derived(parseFloat(data.company.totalBudget));
const income = $derived(parseFloat(data.totalIncome ?? '0'));
const remaining = $derived(total - spent);
const remainingPct = $derived(total > 0 ? (remaining / total) * 100 : 0);
@@ -88,6 +89,16 @@
{total > 0 ? ((allocated / total) * 100).toFixed(1) : '0'}% of total
</p>
</div>
<div class="rounded-lg border border-emerald-300 bg-white p-4 dark:border-emerald-700 dark:bg-gray-800 lg:col-span-4">
<p class="text-xs font-semibold uppercase tracking-wider text-emerald-500 dark:text-emerald-400">
Income (from confirmed sales)
</p>
<p class="mt-1 text-2xl font-bold text-emerald-700 dark:text-emerald-400">
{formatCurrency(income, currency)}
</p>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Net of withholding tax</p>
</div>
</div>
<div class="grid gap-6 lg:grid-cols-2">
@@ -7,7 +7,9 @@ import {
companies,
users,
expenses,
companyLog
companyLog,
sales,
saleLineItems
} from '$lib/server/db/schema.js';
import { and, eq, sql } from 'drizzle-orm';
import { requireCompanyRole } from '$lib/server/authorization.js';
@@ -17,7 +19,7 @@ import { formatCurrency } from '$lib/utils/currency.js';
export const load: PageServerLoad = async ({ parent, params }) => {
const { company } = await parent();
const projectList = await db
const projectListRaw = await db
.select({
id: projects.id,
name: projects.name,
@@ -30,6 +32,36 @@ export const load: PageServerLoad = async ({ parent, params }) => {
.groupBy(projects.id)
.orderBy(projects.name);
// Income per project from confirmed sales (gross - withholding = net receivable)
const incomeRows = await db
.select({
projectId: sales.projectId,
income: sql<string>`coalesce(sum(
(select sum(${saleLineItems.quantity} * ${saleLineItems.unitPrice} * (1 + ${saleLineItems.taxRate})) from sale_line_items where sale_id = ${sales.id})
* (1 - ${sales.withholdingTaxRate})
), '0')::text`
})
.from(sales)
.where(
and(
eq(sales.companyId, params.companyId),
eq(sales.status, 'confirmed')
)
)
.groupBy(sales.projectId);
const incomeByProject = new Map<string | null, string>();
for (const row of incomeRows) {
incomeByProject.set(row.projectId, row.income);
}
const projectList = projectListRaw.map((p) => ({
...p,
income: incomeByProject.get(p.id) ?? '0'
}));
const unassignedIncome = incomeByProject.get(null) ?? '0';
const allocations = await db
.select({
id: budgetAllocations.id,
@@ -64,8 +96,17 @@ export const load: PageServerLoad = async ({ parent, params }) => {
.limit(100);
const totalAllocated = projectList.reduce((s, p) => s + parseFloat(p.allocatedBudget), 0);
const totalIncome =
projectList.reduce((s, p) => s + parseFloat(p.income), 0) + parseFloat(unassignedIncome);
return { projects: projectList, allocations, totalAllocated, changelog };
return {
projects: projectList,
allocations,
totalAllocated,
totalIncome,
unassignedIncome,
changelog
};
};
export const actions: Actions = {