From d5a80bb1048556e68c164ad6de587ba474fb9425 Mon Sep 17 00:00:00 2001 From: David Beccue Date: Mon, 18 May 2026 20:34:30 +0500 Subject: [PATCH] Show sale, COG, and profit on /admin/reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each summary card now leads with profit (large, green, bold), with sale and COG shown underneath as a breakdown. The "Top selling parts" and "Recent sales" tables get Sale/COG/Profit columns, with profit emphasized; negative profit flips both styles to red. Top parts is now ordered by profit. COG is computed from each part's current cost_price for inventory-affecting lines only — custom lines contribute to sale with zero COG. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/i18n/en.json | 3 + src/lib/i18n/tg.json | 3 + src/lib/server/reports.js | 80 ++++++++-------- src/routes/admin/reports/+page.svelte | 126 ++++++++++++++++++-------- 4 files changed, 135 insertions(+), 77 deletions(-) diff --git a/src/lib/i18n/en.json b/src/lib/i18n/en.json index 2b85472..ce614c8 100644 --- a/src/lib/i18n/en.json +++ b/src/lib/i18n/en.json @@ -151,6 +151,9 @@ "top_parts": "Top selling parts", "units_sold": "Units sold", "revenue": "Revenue", + "sale": "Sale", + "cog": "COG", + "profit": "Profit", "recent_sales": "Recent sales", "saved_at": "Saved", "lines": "Lines", diff --git a/src/lib/i18n/tg.json b/src/lib/i18n/tg.json index 0de152d..8539e29 100644 --- a/src/lib/i18n/tg.json +++ b/src/lib/i18n/tg.json @@ -151,6 +151,9 @@ "top_parts": "Қисмҳои серфурӯш", "units_sold": "Фурӯхта шуд", "revenue": "Даромад", + "sale": "Фурӯш", + "cog": "Арзиши мол", + "profit": "Фоида", "recent_sales": "Фурӯшҳои охирин", "saved_at": "Сабт шуд", "lines": "Сатрҳо", diff --git a/src/lib/server/reports.js b/src/lib/server/reports.js index e5d92bd..5897b1e 100644 --- a/src/lib/server/reports.js +++ b/src/lib/server/reports.js @@ -1,45 +1,38 @@ import { getDb } from './db.js'; // All time windows are computed in local time using SQLite's `datetime('now', 'localtime')`. +// Cost of goods (COG) and profit are computed against each part's current cost_price — +// the schema does not snapshot cost at sale time, so historical cost changes are not +// reflected. Custom (non-inventory) lines contribute to sale revenue but have zero COG. + +const COG_SUBQUERY = ` + COALESCE(( + SELECT SUM(l.quantity * p.cost_price) + FROM invoice_lines l + JOIN parts p ON p.id = l.part_id + WHERE l.invoice_id = invoices.id AND l.affects_inventory = 1 + ), 0) +`; + +function windowStats(dateClause) { + return getDb().prepare(` + SELECT + COUNT(*) AS invoice_count, + COALESCE(SUM(total_dirams), 0) AS sale_dirams, + COALESCE(SUM(${COG_SUBQUERY}), 0) AS cog_dirams, + COALESCE(SUM(total_dirams - ${COG_SUBQUERY}), 0) AS profit_dirams + FROM invoices + WHERE status = 'saved'${dateClause ? ' AND ' + dateClause : ''} + `).get(); +} export function salesSummary() { - const db = getDb(); - const row = db.prepare(` - SELECT - COUNT(*) AS invoice_count, - COALESCE(SUM(total_dirams), 0) AS total_dirams - FROM invoices - WHERE status = 'saved' - `).get(); - - const today = db.prepare(` - SELECT - COUNT(*) AS invoice_count, - COALESCE(SUM(total_dirams), 0) AS total_dirams - FROM invoices - WHERE status = 'saved' - AND date(saved_at, 'localtime') = date('now', 'localtime') - `).get(); - - const week = db.prepare(` - SELECT - COUNT(*) AS invoice_count, - COALESCE(SUM(total_dirams), 0) AS total_dirams - FROM invoices - WHERE status = 'saved' - AND date(saved_at, 'localtime') >= date('now', 'localtime', '-6 days') - `).get(); - - const month = db.prepare(` - SELECT - COUNT(*) AS invoice_count, - COALESCE(SUM(total_dirams), 0) AS total_dirams - FROM invoices - WHERE status = 'saved' - AND strftime('%Y-%m', saved_at, 'localtime') = strftime('%Y-%m', 'now', 'localtime') - `).get(); - - return { all_time: row, today, week, month }; + return { + all_time: windowStats(''), + today: windowStats(`date(saved_at, 'localtime') = date('now', 'localtime')`), + week: windowStats(`date(saved_at, 'localtime') >= date('now', 'localtime', '-6 days')`), + month: windowStats(`strftime('%Y-%m', saved_at, 'localtime') = strftime('%Y-%m', 'now', 'localtime')`) + }; } export function topSellingParts(limit = 10) { @@ -47,13 +40,15 @@ export function topSellingParts(limit = 10) { SELECT p.id, p.sku, p.name_en, p.name_tg, SUM(l.quantity) AS units_sold, - SUM(l.quantity * l.unit_price_dirams) AS revenue_dirams + SUM(l.quantity * l.unit_price_dirams) AS sale_dirams, + SUM(l.quantity * p.cost_price) AS cog_dirams, + SUM(l.quantity * (l.unit_price_dirams - p.cost_price)) AS profit_dirams FROM invoice_lines l JOIN invoices i ON i.id = l.invoice_id JOIN parts p ON p.id = l.part_id WHERE i.status = 'saved' AND l.affects_inventory = 1 GROUP BY p.id - ORDER BY units_sold DESC, revenue_dirams DESC + ORDER BY profit_dirams DESC, units_sold DESC LIMIT ? `).all(limit); } @@ -84,7 +79,12 @@ export function inventorySummary() { export function recentSales(limit = 10) { return getDb().prepare(` - SELECT id, total_dirams, saved_at, + SELECT + id, + total_dirams AS sale_dirams, + ${COG_SUBQUERY} AS cog_dirams, + total_dirams - ${COG_SUBQUERY} AS profit_dirams, + saved_at, (SELECT COUNT(*) FROM invoice_lines WHERE invoice_id = invoices.id) AS line_count FROM invoices WHERE status = 'saved' diff --git a/src/routes/admin/reports/+page.svelte b/src/routes/admin/reports/+page.svelte index d79de7b..1d81da7 100644 --- a/src/routes/admin/reports/+page.svelte +++ b/src/routes/admin/reports/+page.svelte @@ -16,38 +16,26 @@

{$t('reports.sales_heading')}

-
-
{$t('reports.today')}
-
- {formatMoney(sales.today.total_dirams, lang)} - {$t('common.currency_short')} + {#each [ + { label: $t('reports.today'), row: sales.today }, + { label: $t('reports.last_7_days'), row: sales.week }, + { label: $t('reports.this_month'), row: sales.month }, + { label: $t('reports.all_time'), row: sales.all_time } + ] as card} +
+
{card.label}
+
{$t('reports.profit')}
+
+ {formatMoney(card.row.profit_dirams, lang)} + {$t('common.currency_short')} +
+
+
{$t('reports.sale')} {formatMoney(card.row.sale_dirams, lang)}
+
{$t('reports.cog')} {formatMoney(card.row.cog_dirams, lang)}
+
+
{card.row.invoice_count} {$t('reports.invoices')}
-
{sales.today.invoice_count} {$t('reports.invoices')}
-
-
-
{$t('reports.last_7_days')}
-
- {formatMoney(sales.week.total_dirams, lang)} - {$t('common.currency_short')} -
-
{sales.week.invoice_count} {$t('reports.invoices')}
-
-
-
{$t('reports.this_month')}
-
- {formatMoney(sales.month.total_dirams, lang)} - {$t('common.currency_short')} -
-
{sales.month.invoice_count} {$t('reports.invoices')}
-
-
-
{$t('reports.all_time')}
-
- {formatMoney(sales.all_time.total_dirams, lang)} - {$t('common.currency_short')} -
-
{sales.all_time.invoice_count} {$t('reports.invoices')}
-
+ {/each}

{$t('reports.inventory_heading')}

@@ -95,7 +83,9 @@ {$t('parts.sku')} {$t('parts.name')} {$t('reports.units_sold')} - {$t('reports.revenue')} + {$t('reports.sale')} + {$t('reports.cog')} + {$t('reports.profit')} @@ -105,7 +95,15 @@ {localized(p, 'name', lang)} {p.units_sold} - {formatMoney(p.revenue_dirams, lang)} + {formatMoney(p.sale_dirams, lang)} + {$t('common.currency_short')} + + + {formatMoney(p.cog_dirams, lang)} + {$t('common.currency_short')} + + + {formatMoney(p.profit_dirams, lang)} {$t('common.currency_short')} @@ -123,7 +121,9 @@ {$t('reports.saved_at')} {$t('reports.lines')} - {$t('common.total')} + {$t('reports.sale')} + {$t('reports.cog')} + {$t('reports.profit')} @@ -133,7 +133,15 @@ {formatWhen(s.saved_at)} {s.line_count} - {formatMoney(s.total_dirams, lang)} + {formatMoney(s.sale_dirams, lang)} + {$t('common.currency_short')} + + + {formatMoney(s.cog_dirams, lang)} + {$t('common.currency_short')} + + + {formatMoney(s.profit_dirams, lang)} {$t('common.currency_short')} {$t('reports.view')} @@ -146,7 +154,7 @@