From c9096dfd844230e885068001d3da8ef17bb98301 Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 1 Sep 2026 01:55:52 +0200 Subject: [PATCH] feat(dashboard2): trim Row 1 to Selling-only with last-month delta MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop Cost and Markup gauge — Selling is the headline the rep cares about - monthRevenue is now {sellingThisMonth, sellingLastMonth} - One combined PB query covers both months; classify each row by date in code so the delta costs zero extra round-trips - RepKpiCard gains a format="currency" prop so Selling renders ZAR-formatted with a delta arrow (mirrors the quote-count cards) - Row 1 grid adjusts from 6-col to 4-col to match the new card count --- src/app/(app)/dashboard2/page.tsx | 75 ++++---------------- src/lib/server/rep-dashboard-service.test.ts | 25 ++++--- src/lib/server/rep-dashboard-service.ts | 32 +++++---- 3 files changed, 44 insertions(+), 88 deletions(-) diff --git a/src/app/(app)/dashboard2/page.tsx b/src/app/(app)/dashboard2/page.tsx index 06f0933..8d81146 100644 --- a/src/app/(app)/dashboard2/page.tsx +++ b/src/app/(app)/dashboard2/page.tsx @@ -9,8 +9,6 @@ import { ShoppingCart, CalendarDays, TrendingUp, - Gauge, - CheckCircle, AlertCircle, DollarSign, } from "lucide-react"; @@ -19,7 +17,6 @@ import getPocketBase from "@/lib/pocketbase"; import { CHART, withAlpha } from "@/components/charts/palette"; import { DashboardCard } from "@/components/charts/dashboard-card"; import { BarChart } from "@/components/charts/bar-chart"; -import { RadialGauge } from "@/components/charts/radial-gauge"; import type { RepDashboardData } from "@/lib/server/rep-dashboard-service"; function weekLabel(iso: string): string { @@ -114,8 +111,8 @@ export default function RepDashboardPage() {

- {/* Row 1: quotes-loaded KPIs + this month's money (6 cards) */} -
+ {/* Row 1: quotes-loaded KPIs + this month's selling (4 cards) */} +
- - - - {monthRevenue.markupPct === null ? ( -
- -

No orders yet this month

-
- ) : ( - - )} -
{/* Row 2: trend + outstanding lists */} @@ -215,14 +190,18 @@ function RepKpiCard({ delta, icon: Icon, accentColor, + format = "number", }: { label: string; value: number; delta: number; icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>; accentColor: string; + format?: "number" | "currency"; }) { const positive = delta >= 0; + const display = + format === "currency" ? formatZar(value) : value.toLocaleString(); return (
@@ -235,7 +214,7 @@ function RepKpiCard({
- {value.toLocaleString()} + {display}
{positive ? "+" : ""} @@ -245,36 +224,6 @@ function RepKpiCard({ ); } -function RepCurrencyKpiCard({ - label, - value, - icon: Icon, - accentColor, -}: { - label: string; - value: number; - icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>; - accentColor: string; -}) { - return ( - -
-
{label}
-
- -
-
-
- {formatZar(value)} -
-
This month
-
- ); -} - function RepList({ items, emptyLabel, diff --git a/src/lib/server/rep-dashboard-service.test.ts b/src/lib/server/rep-dashboard-service.test.ts index e2c2b5e..5ff8f08 100644 --- a/src/lib/server/rep-dashboard-service.test.ts +++ b/src/lib/server/rep-dashboard-service.test.ts @@ -191,33 +191,32 @@ describe("getRepDashboardData", () => { expect(data.outstandingOrders.items.map((i) => i.order_number).sort()).toEqual(["ORD1", "ORD2"]); }); - it("sums selling/cost and computes markup % for this month's order items", async () => { + it("sums selling for this month and last month in a single query", async () => { seed("quotes", []); seed("orders", []); seed("order_items", [ - { id: "i1", "order.customer.rep": REP_ID, created: "2026-08-05T00:00:00.000Z", quantity: 2, selling_price: 100, cost_price: 50 }, - { id: "i2", "order.customer.rep": REP_ID, created: "2026-08-10T00:00:00.000Z", quantity: 1, selling_price: 40, cost_price: 20 }, - { id: "i3", "order.customer.rep": REP_ID, created: "2026-07-31T00:00:00.000Z", quantity: 1, selling_price: 999, cost_price: 1 }, // last month, excluded - { id: "i4", "order.customer.rep": OTHER_REP_ID, created: "2026-08-05T00:00:00.000Z", quantity: 1, selling_price: 999, cost_price: 1 }, // different rep + { id: "i1", "order.customer.rep": REP_ID, created: "2026-08-05T00:00:00.000Z", quantity: 2, selling_price: 100 }, + { id: "i2", "order.customer.rep": REP_ID, created: "2026-08-10T00:00:00.000Z", quantity: 1, selling_price: 40 }, + { id: "i3", "order.customer.rep": REP_ID, created: "2026-07-31T00:00:00.000Z", quantity: 1, selling_price: 999 }, // last month + { id: "i4", "order.customer.rep": OTHER_REP_ID, created: "2026-08-05T00:00:00.000Z", quantity: 1, selling_price: 999 }, // different rep + { id: "i5", "order.customer.rep": REP_ID, created: "2026-06-15T00:00:00.000Z", quantity: 1, selling_price: 500 }, // outside both months ]); const data = await getRepDashboardData(REP_ID, NOW, pbMock as never); - // selling = 2*100 + 1*40 = 240; cost = 2*50 + 1*20 = 120 - expect(data.monthRevenue.selling).toBe(240); - expect(data.monthRevenue.cost).toBe(120); - expect(data.monthRevenue.markupPct).toBe(100); // (240-120)/120 * 100 + // this month = 2*100 + 1*40 = 240; last month = 1*999 = 999 + expect(data.monthRevenue.sellingThisMonth).toBe(240); + expect(data.monthRevenue.sellingLastMonth).toBe(999); }); - it("returns markupPct: null when there's no cost this month (no div-by-zero)", async () => { + it("returns zeroes when there are no order items this month or last", async () => { seed("quotes", []); seed("orders", []); seed("order_items", []); const data = await getRepDashboardData(REP_ID, NOW, pbMock as never); - expect(data.monthRevenue.selling).toBe(0); - expect(data.monthRevenue.cost).toBe(0); - expect(data.monthRevenue.markupPct).toBeNull(); + expect(data.monthRevenue.sellingThisMonth).toBe(0); + expect(data.monthRevenue.sellingLastMonth).toBe(0); }); }); \ No newline at end of file diff --git a/src/lib/server/rep-dashboard-service.ts b/src/lib/server/rep-dashboard-service.ts index 131a964..5648544 100644 --- a/src/lib/server/rep-dashboard-service.ts +++ b/src/lib/server/rep-dashboard-service.ts @@ -31,9 +31,8 @@ export interface RepDashboardData { items: { id: string; order_number: string; customer_name: string | null }[]; }; monthRevenue: { - selling: number; - cost: number; - markupPct: number | null; + sellingThisMonth: number; + sellingLastMonth: number; }; } @@ -172,8 +171,10 @@ export async function getRepDashboardData( }, ), pb.collection(getOrderItemsCollectionName()).getList(1, MONTH_ORDER_ITEM_LIMIT, { - filter: `${orderRepFilter} && ${rangeFilter("created", thisMonthStart, nextMonthStart)}`, - fields: "id,quantity,order_qty,selling_price,cost_price", + // Single query covers both months; classify each row by `created` + // in code so we don't pay a second round-trip just for the delta. + filter: `${orderRepFilter} && created >= "${lastMonthStart.toISOString()}" && created < "${nextMonthStart.toISOString()}"`, + fields: "id,created,quantity,order_qty,selling_price", }), ]); @@ -227,19 +228,26 @@ export async function getRepDashboardData( }; /* ---------- monthRevenue ---------- */ - let selling = 0; - let cost = 0; + let sellingThisMonth = 0; + let sellingLastMonth = 0; + const thisMonthStartMs = thisMonthStart.getTime(); for (const it of monthOrderItems.items as unknown as Array<{ + created?: string; quantity?: number | null; order_qty?: number | null; selling_price?: number | null; - cost_price?: number | null; }>) { const qty = Number(it.quantity ?? it.order_qty ?? 0); - selling += Number(it.selling_price ?? 0) * qty; - cost += Number(it.cost_price ?? 0) * qty; + const lineTotal = Number(it.selling_price ?? 0) * qty; + // `created` is an ISO string — compare on its parsed timestamp so we + // don't tie this to a particular date-string format PB happens to use. + const createdMs = it.created ? new Date(it.created).getTime() : 0; + if (createdMs >= thisMonthStartMs) { + sellingThisMonth += lineTotal; + } else { + sellingLastMonth += lineTotal; + } } - const markupPct = cost > 0 ? ((selling - cost) / cost) * 100 : null; return { quotesLoaded: { @@ -253,6 +261,6 @@ export async function getRepDashboardData( quoteVolumeTrend, outstandingQuotes, outstandingOrders, - monthRevenue: { selling, cost, markupPct }, + monthRevenue: { sellingThisMonth, sellingLastMonth }, }; } \ No newline at end of file