change average price to median price on the dashboard (#300)

* change average price to median price on the dashboard

* Use more efficient median calculation

Co-authored-by: Christian Kellner <weakmap@gmail.com>

* Fix applied suggestion artifacts

* Update sql query and js sort function

* Group sql statement by id

* Revert sort function change

---------

Co-authored-by: Christian Kellner <weakmap@gmail.com>
This commit is contained in:
bytedream
2026-04-20 10:13:11 +02:00
committed by GitHub
parent 522bbc2282
commit cc0164b689
3 changed files with 35 additions and 21 deletions

View File

@@ -37,7 +37,7 @@ dashboardRouter.get('/', async (req, res) => {
const totalJobs = jobs.length; const totalJobs = jobs.length;
const totalListings = jobs.reduce((sum, j) => sum + (j.numberOfFoundListings || 0), 0); const totalListings = jobs.reduce((sum, j) => sum + (j.numberOfFoundListings || 0), 0);
const jobIds = jobs.map((j) => j.id); const jobIds = jobs.map((j) => j.id);
const { numberOfActiveListings, avgPriceOfListings } = getListingsKpisForJobIds(jobIds); const { numberOfActiveListings, medianPriceOfListings } = getListingsKpisForJobIds(jobIds);
// Build Pie data in a simple shape the frontend can consume directly // Build Pie data in a simple shape the frontend can consume directly
// Shape: { labels: string[], values: number[] } with values as percentages // Shape: { labels: string[], values: number[] } with values as percentages
const providerPieRaw = getProviderDistributionForJobIds(jobIds); const providerPieRaw = getProviderDistributionForJobIds(jobIds);
@@ -63,7 +63,7 @@ dashboardRouter.get('/', async (req, res) => {
totalJobs, totalJobs,
totalListings, totalListings,
numberOfActiveListings, numberOfActiveListings,
avgPriceOfListings, medianPriceOfListings,
}, },
pie: providerPie, pie: providerPie,
}; };

View File

@@ -29,33 +29,47 @@ export const getKnownListingHashesForJobAndProvider = (jobId, providerId) => {
* Compute KPI aggregates for a given set of job IDs from the listings table. * Compute KPI aggregates for a given set of job IDs from the listings table.
* *
* - numberOfActiveListings: count of listings where is_active = 1 * - numberOfActiveListings: count of listings where is_active = 1
* - avgPriceOfListings: average of numeric price, rounded to nearest integer * - medianPriceOfListings: median of numeric price, rounded to nearest integer
* *
* When no jobIds are provided, returns zeros. * When no jobIds are provided, returns zeros.
* *
* @param {string[]} jobIds * @param {string[]} jobIds
* @returns {{ numberOfActiveListings: number, avgPriceOfListings: number }} * @returns {{ numberOfActiveListings: number, medianPriceOfListings: number }}
*/ */
export const getListingsKpisForJobIds = (jobIds = []) => { export const getListingsKpisForJobIds = (jobIds = []) => {
if (!Array.isArray(jobIds) || jobIds.length === 0) { if (!Array.isArray(jobIds) || jobIds.length === 0) {
return { numberOfActiveListings: 0, avgPriceOfListings: 0 }; return { numberOfActiveListings: 0, medianPriceOfListings: 0 };
} }
const placeholders = jobIds.map(() => '?').join(','); const placeholders = jobIds.map(() => '?').join(',');
const row = const rows = SqliteConnection.query(
SqliteConnection.query( `SELECT
`SELECT SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) OVER() AS active_count,
SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) AS activeCount, price
AVG(price) AS avgPrice FROM listings
FROM listings WHERE job_id IN (${placeholders})
WHERE job_id IN (${placeholders}) AND manually_deleted = 0
AND manually_deleted = 0`, GROUP BY
jobIds, id`,
)[0] || {}; jobIds,
);
const activeCount = rows[0]?.active_count ?? 0;
const prices = rows
.map((r) => r.price)
.filter((p) => p !== null)
.sort((a, b) => a - b);
let medianPrice = 0;
if (prices.length > 0) {
const mid = Math.floor(prices.length / 2);
medianPrice = prices.length % 2 !== 0 ? prices[mid] : (prices[mid - 1] + prices[mid]) / 2;
}
return { return {
numberOfActiveListings: Number(row.activeCount || 0), numberOfActiveListings: activeCount,
avgPriceOfListings: row?.avgPrice == null ? 0 : Math.round(Number(row.avgPrice)), medianPriceOfListings: medianPrice,
}; };
}; };

View File

@@ -127,18 +127,18 @@ export default function Dashboard() {
</Col> </Col>
<Col xs={24} sm={12} md={12} lg={6} xl={6}> <Col xs={24} sm={12} md={12} lg={6} xl={6}>
<KpiCard <KpiCard
title="Avg. Price" title="Median Price"
color="purple" color="purple"
value={`${ value={`${
!kpis.avgPriceOfListings !kpis.medianPriceOfListings
? '---' ? '---'
: new Intl.NumberFormat('de-DE', { : new Intl.NumberFormat('de-DE', {
style: 'currency', style: 'currency',
currency: 'EUR', currency: 'EUR',
}).format(kpis.avgPriceOfListings) }).format(kpis.medianPriceOfListings)
}`} }`}
icon={<IconNoteMoney />} icon={<IconNoteMoney />}
description="Avg. Price of listings" description="Median Price of listings"
/> />
</Col> </Col>
</Row> </Row>