From 0795d78bdffb1940d0c110ca95541f6512a4d5fc Mon Sep 17 00:00:00 2001
From: grabowski
Date: Mon, 20 Apr 2026 12:48:06 +0700
Subject: [PATCH] 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)
---
.../companies/[companyId]/+page.server.ts | 19 +++++++-
.../(app)/companies/[companyId]/+page.svelte | 11 +++++
.../[companyId]/budget/+page.server.ts | 47 +++++++++++++++++--
3 files changed, 72 insertions(+), 5 deletions(-)
diff --git a/src/routes/(app)/companies/[companyId]/+page.server.ts b/src/routes/(app)/companies/[companyId]/+page.server.ts
index d3af353..8a7e905 100644
--- a/src/routes/(app)/companies/[companyId]/+page.server.ts
+++ b/src/routes/(app)/companies/[companyId]/+page.server.ts
@@ -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`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'
+ };
};
diff --git a/src/routes/(app)/companies/[companyId]/+page.svelte b/src/routes/(app)/companies/[companyId]/+page.svelte
index d3c06eb..812103a 100644
--- a/src/routes/(app)/companies/[companyId]/+page.svelte
+++ b/src/routes/(app)/companies/[companyId]/+page.svelte
@@ -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
+
+
+
+ Income (from confirmed sales)
+
+
+ {formatCurrency(income, currency)}
+
+
Net of withholding tax
+
diff --git a/src/routes/(app)/companies/[companyId]/budget/+page.server.ts b/src/routes/(app)/companies/[companyId]/budget/+page.server.ts
index 7e60ea1..b30b080 100644
--- a/src/routes/(app)/companies/[companyId]/budget/+page.server.ts
+++ b/src/routes/(app)/companies/[companyId]/budget/+page.server.ts
@@ -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`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();
+ 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 = {