From 1fc3580aed9e330b134a76e3f82269ee8019d4e6 Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 1 Sep 2026 02:08:52 +0200 Subject: [PATCH] perf(dashboard2): consolidate 14 PB count queries into 1 fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single getList pulls every quote this rep created in the last 62 days, then buckets client-side into the 6 day/week/month windows plus the 8-week trend. Also fans out the 4 remaining queries (outstandingQuotes, outstandingOrderItems probe, monthRevenue, the consolidated quotes fetch) in parallel via Promise.all. Query count: 18 → 5. Page load bounded by the slowest query instead of the sum. The widest quote window is "last month" (~62 days), so that single bound covers every other day/week/month window for bucketing. Cap at 2000 rows to defend against runaway responses for very active reps. --- src/lib/server/rep-dashboard-service.ts | 130 +++++++++++++----------- 1 file changed, 70 insertions(+), 60 deletions(-) diff --git a/src/lib/server/rep-dashboard-service.ts b/src/lib/server/rep-dashboard-service.ts index 5648544..fb38dbf 100644 --- a/src/lib/server/rep-dashboard-service.ts +++ b/src/lib/server/rep-dashboard-service.ts @@ -9,6 +9,15 @@ const QUOTE_VOLUME_WEEKS = 8; const OUTSTANDING_LIST_CAP = 8; const OUTSTANDING_ORDER_ITEMS_CAP = 200; const MONTH_ORDER_ITEM_LIMIT = 1000; +// Quote-volume trend + the 6 day/week/month windows are all bucketed +// from a single fetch — the widest window is "last month" (~62 days), +// so this bound covers every other window. For a single rep this is a +// small slice; even busy reps rarely exceed a few hundred quotes over +// 2 months. +const QUOTE_FETCH_LOOKBACK_MS = 62 * 24 * 60 * 60 * 1000; +// Hard cap on the single fetch in case a rep really has thousands of +// quotes in 2 months — protects the page from a runaway response. +const QUOTE_FETCH_LIMIT = 2000; type AdminPocketBase = Awaited>; @@ -71,21 +80,13 @@ function esc(value: string): string { return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); } -function rangeFilter(field: string, start: Date, end: Date): string { - return `${field} >= "${start.toISOString()}" && ${field} < "${end.toISOString()}"`; -} - -/** Count-only query — reads PB's own `totalItems` via a `perPage=1` page. */ -async function countWhere( - pb: AdminPocketBase, - collection: string, - filter: string, -): Promise { - const result = await pb.collection(collection).getList(1, 1, { - filter, - fields: "id", - }); - return result.totalItems || 0; +/** Build the 8 week-start dates for the trend chart, oldest first. */ +function buildWeekStarts(thisWeekStart: Date): Date[] { + const out: Date[] = []; + for (let i = QUOTE_VOLUME_WEEKS - 1; i >= 0; i--) { + out.push(addDaysUtc(thisWeekStart, -7 * i)); + } + return out; } export async function getRepDashboardData( @@ -99,7 +100,7 @@ export async function getRepDashboardData( const repFilter = `customer.rep = "${esc(repId)}"`; const orderRepFilter = `order.customer.rep = "${esc(repId)}"`; - /* ---------- quotesLoaded (6 count-only queries) ---------- */ + /* ---------- date boundaries (UTC) ---------- */ const todayStart = startOfDayUtc(now); const yesterdayStart = addDaysUtc(todayStart, -1); const tomorrowStart = addDaysUtc(todayStart, 1); @@ -109,53 +110,36 @@ export async function getRepDashboardData( const thisMonthStart = startOfMonthUtc(now); const lastMonthStart = addMonthsUtc(thisMonthStart, -1); const nextMonthStart = addMonthsUtc(thisMonthStart, 1); + const quoteWindowStart = new Date(now.getTime() - QUOTE_FETCH_LOOKBACK_MS); + + const todayStartMs = todayStart.getTime(); + const yesterdayStartMs = yesterdayStart.getTime(); + const thisWeekStartMs = thisWeekStart.getTime(); + const lastWeekStartMs = lastWeekStart.getTime(); + const thisMonthStartMs = thisMonthStart.getTime(); + const lastMonthStartMs = lastMonthStart.getTime(); + const nextMonthStartMs = nextMonthStart.getTime(); + const nextWeekStartMs = nextWeekStart.getTime(); const quoteBaseFilter = `${repFilter} && is_template != true`; + const weekStarts = buildWeekStarts(thisWeekStart); - const [ - todayCount, - yesterdayCount, - thisWeekCount, - lastWeekCount, - thisMonthCount, - lastMonthCount, - ] = await Promise.all([ - countWhere(pb, QUOTES_COLLECTION, `${quoteBaseFilter} && ${rangeFilter("created", todayStart, tomorrowStart)}`), - countWhere(pb, QUOTES_COLLECTION, `${quoteBaseFilter} && ${rangeFilter("created", yesterdayStart, todayStart)}`), - countWhere(pb, QUOTES_COLLECTION, `${quoteBaseFilter} && ${rangeFilter("created", thisWeekStart, nextWeekStart)}`), - countWhere(pb, QUOTES_COLLECTION, `${quoteBaseFilter} && ${rangeFilter("created", lastWeekStart, thisWeekStart)}`), - countWhere(pb, QUOTES_COLLECTION, `${quoteBaseFilter} && ${rangeFilter("created", thisMonthStart, nextMonthStart)}`), - countWhere(pb, QUOTES_COLLECTION, `${quoteBaseFilter} && ${rangeFilter("created", lastMonthStart, thisMonthStart)}`), - ]); - - /* ---------- quoteVolumeTrend (8 weekly count-only queries) ---------- */ - const weekStarts: Date[] = []; - for (let i = QUOTE_VOLUME_WEEKS - 1; i >= 0; i--) { - weekStarts.push(addDaysUtc(thisWeekStart, -7 * i)); - } - const quoteVolumeTrend = await Promise.all( - weekStarts.map(async (weekStart) => { - const weekEnd = addDaysUtc(weekStart, 7); - const count = await countWhere( - pb, - QUOTES_COLLECTION, - `${quoteBaseFilter} && ${rangeFilter("created", weekStart, weekEnd)}`, - ); - return { weekStart: weekStart.toISOString().slice(0, 10), count }; - }), - ); - - /* ---------- outstandingQuotes, outstandingOrders probe, monthRevenue ---------- - * These three queries are independent — fan them out in parallel so the - * page load is bounded by the slowest one (~80-150ms) instead of their - * sum (~300-500ms). The follow-up `orders` fetch for outstandingOrders - * stays sequential because it needs the deduped order ids from the probe. + /* ---------- 4 independent queries in parallel ---------- + * 1. quotesLoaded + quoteVolumeTrend (single fetch, bucket client-side) + * 2. outstandingQuotes (capped list) + * 3. outstandingOrderItems probe (capped at 200 items) + * 4. monthRevenue (covers both months in one fetch) */ const [ + recentQuotesResult, outstandingQuotesResult, outstandingOrderItems, monthOrderItems, ] = await Promise.all([ + pb.collection(QUOTES_COLLECTION).getList<{ created?: string }>(1, QUOTE_FETCH_LIMIT, { + filter: `${quoteBaseFilter} && created >= "${quoteWindowStart.toISOString()}"`, + fields: "id,created", + }), pb.collection(QUOTES_COLLECTION).getList(1, OUTSTANDING_LIST_CAP, { filter: `${repFilter} && (status = "new" || status = "claimed") && is_template != true`, sort: "-created", @@ -178,6 +162,35 @@ export async function getRepDashboardData( }), ]); + /* ---------- bucket quotesLoaded + quoteVolumeTrend from the single fetch ---------- */ + let todayCount = 0; + let yesterdayCount = 0; + let thisWeekCount = 0; + let lastWeekCount = 0; + let thisMonthCount = 0; + let lastMonthCount = 0; + const weekBuckets = new Array(QUOTE_VOLUME_WEEKS).fill(0); + + for (const q of recentQuotesResult.items) { + if (!q.created) continue; + const ms = new Date(q.created).getTime(); + if (ms >= todayStartMs) todayCount++; + else if (ms >= yesterdayStartMs) yesterdayCount++; + if (ms >= thisWeekStartMs && ms < nextWeekStartMs) thisWeekCount++; + else if (ms >= lastWeekStartMs && ms < thisWeekStartMs) lastWeekCount++; + if (ms >= thisMonthStartMs && ms < nextMonthStartMs) thisMonthCount++; + else if (ms >= lastMonthStartMs && ms < thisMonthStartMs) lastMonthCount++; + // 8-week trend: index 0 = current week, 1 = last week, ... 7 = 7 weeks ago + const weeksAgo = Math.floor((thisWeekStartMs - ms) / (7 * 24 * 60 * 60 * 1000)); + if (weeksAgo >= 0 && weeksAgo < QUOTE_VOLUME_WEEKS) { + weekBuckets[weeksAgo]++; + } + } + const quoteVolumeTrend = weekStarts.map((weekStart, i) => ({ + weekStart: weekStart.toISOString().slice(0, 10), + count: weekBuckets[i], + })); + /* ---------- outstandingQuotes ---------- */ const outstandingQuotes = { total: outstandingQuotesResult.totalItems || 0, @@ -230,7 +243,6 @@ export async function getRepDashboardData( /* ---------- monthRevenue ---------- */ 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; @@ -239,12 +251,10 @@ export async function getRepDashboardData( }>) { const qty = Number(it.quantity ?? it.order_qty ?? 0); 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) { + const ms = it.created ? new Date(it.created).getTime() : 0; + if (ms >= thisMonthStartMs && ms < nextMonthStartMs) { sellingThisMonth += lineTotal; - } else { + } else if (ms >= lastMonthStartMs && ms < thisMonthStartMs) { sellingLastMonth += lineTotal; } }