adding new dashboard view. Muchas wow

This commit is contained in:
orangecoding
2025-12-14 12:23:59 +01:00
parent 87b5673bf0
commit 87771655a8
31 changed files with 688 additions and 1047 deletions

View File

@@ -7,40 +7,6 @@ import { nullOrEmpty } from '../../utils.js';
import SqliteConnection from './SqliteConnection.js';
import { nanoid } from 'nanoid';
/**
* Build analytics data for a given job by grouping all listings by provider and
* mapping each listing hash to its creation timestamp.
*
* SQL shape:
* SELECT json_group_object(provider, json_object(hash, created_at)) AS result
* FROM listings WHERE job_id = @jobId;
*
* The resulting object has the shape:
* {
* providerA: { "<hash1>": <created_at_ms>, "<hash2>": <created_at_ms>, ... },
* providerB: { ... }
* }
*
* @param {string} jobId - ID of the job whose listings should be aggregated.
* @returns {Record<string, Record<string, number>>} Object grouped by provider mapping listing-hash -> created_at epoch ms.
*/
export const getListingProviderDataForAnalytics = (jobId) => {
const row = SqliteConnection.query(
`SELECT COALESCE(
json_group_object(provider, json(provider_map)),
json('{}')
) AS result
FROM (SELECT provider,
json_group_object(hash, created_at) AS provider_map
FROM listings
WHERE job_id = @jobId
GROUP BY provider);`,
{ jobId },
);
return row?.length > 0 ? JSON.parse(row[0].result) : {};
};
/**
* Return a list of known listing hashes for a given job and provider.
* Useful to de-duplicate before inserting new listings.
@@ -59,6 +25,89 @@ export const getKnownListingHashesForJobAndProvider = (jobId, providerId) => {
).map((r) => r.hash);
};
/**
* Compute KPI aggregates for a given set of job IDs from the listings table.
*
* - numberOfActiveListings: count of listings where is_active = 1
* - avgPriceOfListings: average of numeric price, rounded to nearest integer
*
* When no jobIds are provided, returns zeros.
*
* @param {string[]} jobIds
* @returns {{ numberOfActiveListings: number, avgPriceOfListings: number }}
*/
export const getListingsKpisForJobIds = (jobIds = []) => {
if (!Array.isArray(jobIds) || jobIds.length === 0) {
return { numberOfActiveListings: 0, avgPriceOfListings: 0 };
}
const placeholders = jobIds.map(() => '?').join(',');
const row =
SqliteConnection.query(
`SELECT
SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) AS activeCount,
AVG(price) AS avgPrice
FROM listings
WHERE job_id IN (${placeholders})`,
jobIds,
)[0] || {};
return {
numberOfActiveListings: Number(row.activeCount || 0),
avgPriceOfListings: row?.avgPrice == null ? 0 : Math.round(Number(row.avgPrice)),
};
};
/**
* Compute distribution of listings by provider for the given set of job IDs.
* Returns data ready for the pie chart component with fields `type` and `value` (percentage).
*
* Example return:
* [ { type: 'immoscout', value: 62 }, { type: 'immowelt', value: 38 } ]
*
* When no jobIds are provided or no listings exist, returns empty array.
*
* @param {string[]} jobIds
* @returns {{ type: string, value: number }[]}
*/
export const getProviderDistributionForJobIds = (jobIds = []) => {
if (!Array.isArray(jobIds) || jobIds.length === 0) {
return [];
}
const placeholders = jobIds.map(() => '?').join(',');
const rows = SqliteConnection.query(
`SELECT provider, COUNT(*) AS cnt
FROM listings
WHERE job_id IN (${placeholders})
GROUP BY provider
ORDER BY cnt DESC`,
jobIds,
);
const total = rows.reduce((acc, r) => acc + Number(r.cnt || 0), 0);
if (total === 0) return [];
// Map counts to integer percentage values (0-100). Ensure sum is ~100 by rounding.
const percentages = rows.map((r) => ({
type: r.provider,
value: Math.round((Number(r.cnt) / total) * 100),
}));
// Adjust rounding drift to keep sum at 100 (optional minor correction)
const drift = 100 - percentages.reduce((s, p) => s + p.value, 0);
if (drift !== 0 && percentages.length > 0) {
// apply drift to the largest slice to keep UX simple
let maxIdx = 0;
for (let i = 1; i < percentages.length; i++) {
if (percentages[i].value > percentages[maxIdx].value) maxIdx = i;
}
percentages[maxIdx].value = Math.max(0, percentages[maxIdx].value + drift);
}
return percentages;
};
/**
* Return a list of listing that either are active or have an unknown status
* to constantly check if they are still online