mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
adding new dashboard view. Muchas wow
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user