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:
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user