feat: Implement initial API backend with a comprehensive PostgreSQL schema, Express application, and data synchronization for articles and ranking.
This commit is contained in:
1 parent
e189cc3045
commit
9675a243b7
4 files changed
+165
-2
No files matched your search
@@ -310,3 +310,16 @@ CREATE TABLE IF NOT EXISTS cdefou_receplig (
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_receplig_artfou1 ON cdefou_receplig (artfou1_no_id);
|
CREATE INDEX IF NOT EXISTS idx_receplig_artfou1 ON cdefou_receplig (artfou1_no_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_receplig_date ON cdefou_receplig (suividatecreation);
|
CREATE INDEX IF NOT EXISTS idx_receplig_date ON cdefou_receplig (suividatecreation);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- CA journalier + trafic (StatOpCAJour SQL Server)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS statopca (
|
||||||
|
site TEXT NOT NULL,
|
||||||
|
datmvt DATE NOT NULL,
|
||||||
|
mnt NUMERIC(14,4),
|
||||||
|
nbticket INTEGER,
|
||||||
|
PRIMARY KEY (site, datmvt)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_statopca_datmvt ON statopca (datmvt);
|
||||||
@@ -153,4 +153,103 @@ router.get('/ca/gamme', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/performance/dashboard?date=2026-03-20&site=
|
||||||
|
// CA + trafic du jour vs N-1 + top 10 CA / qté / marge
|
||||||
|
router.get('/dashboard', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { date, site = '' } = req.query;
|
||||||
|
|
||||||
|
const targetDate = date || new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
||||||
|
const n1Date = new Date(targetDate);
|
||||||
|
n1Date.setFullYear(n1Date.getFullYear() - 1);
|
||||||
|
const n1DateStr = n1Date.toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
const siteFilter = site ? `AND site = $3` : '';
|
||||||
|
const siteParams = site ? [targetDate, n1DateStr, site] : [targetDate, n1DateStr];
|
||||||
|
|
||||||
|
const [caRows, top10CA, top10Qte, top10Marge] = await Promise.all([
|
||||||
|
// CA + trafic N et N-1
|
||||||
|
pool.query(`
|
||||||
|
SELECT site, datmvt::DATE AS date, mnt AS ca_ttc, nbticket AS trafic
|
||||||
|
FROM statopca
|
||||||
|
WHERE datmvt IN ($1, $2) ${siteFilter}
|
||||||
|
ORDER BY site, datmvt
|
||||||
|
`, siteParams),
|
||||||
|
|
||||||
|
// Top 10 CA
|
||||||
|
pool.query(`
|
||||||
|
SELECT a.codein, a.libelle1, m.site,
|
||||||
|
ABS(SUM(m.mntmvtttc)) AS ca_ttc,
|
||||||
|
ABS(SUM(m.qtemvt)) AS qte_vendue,
|
||||||
|
SUM(m.margemvt) AS marge
|
||||||
|
FROM mvtart m
|
||||||
|
JOIN articles a ON a.no_id = m.artnoid
|
||||||
|
WHERE m.datmvt::DATE = $1 AND m.genremvt = 3 ${site ? 'AND m.site = $2' : ''}
|
||||||
|
GROUP BY a.codein, a.libelle1, m.site
|
||||||
|
ORDER BY ca_ttc DESC LIMIT 10
|
||||||
|
`, site ? [targetDate, site] : [targetDate]),
|
||||||
|
|
||||||
|
// Top 10 Qté
|
||||||
|
pool.query(`
|
||||||
|
SELECT a.codein, a.libelle1, m.site,
|
||||||
|
ABS(SUM(m.mntmvtttc)) AS ca_ttc,
|
||||||
|
ABS(SUM(m.qtemvt)) AS qte_vendue,
|
||||||
|
SUM(m.margemvt) AS marge
|
||||||
|
FROM mvtart m
|
||||||
|
JOIN articles a ON a.no_id = m.artnoid
|
||||||
|
WHERE m.datmvt::DATE = $1 AND m.genremvt = 3 ${site ? 'AND m.site = $2' : ''}
|
||||||
|
GROUP BY a.codein, a.libelle1, m.site
|
||||||
|
ORDER BY qte_vendue DESC LIMIT 10
|
||||||
|
`, site ? [targetDate, site] : [targetDate]),
|
||||||
|
|
||||||
|
// Top 10 Marge
|
||||||
|
pool.query(`
|
||||||
|
SELECT a.codein, a.libelle1, m.site,
|
||||||
|
ABS(SUM(m.mntmvtttc)) AS ca_ttc,
|
||||||
|
ABS(SUM(m.qtemvt)) AS qte_vendue,
|
||||||
|
SUM(m.margemvt) AS marge
|
||||||
|
FROM mvtart m
|
||||||
|
JOIN articles a ON a.no_id = m.artnoid
|
||||||
|
WHERE m.datmvt::DATE = $1 AND m.genremvt = 3 ${site ? 'AND m.site = $2' : ''}
|
||||||
|
GROUP BY a.codein, a.libelle1, m.site
|
||||||
|
ORDER BY marge DESC LIMIT 10
|
||||||
|
`, site ? [targetDate, site] : [targetDate]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Construire la comparaison N vs N-1 par site
|
||||||
|
const byDate = {};
|
||||||
|
for (const row of caRows.rows) {
|
||||||
|
const d = row.date instanceof Date ? row.date.toISOString().slice(0, 10) : String(row.date).slice(0, 10);
|
||||||
|
if (!byDate[row.site]) byDate[row.site] = {};
|
||||||
|
byDate[row.site][d] = { ca_ttc: parseFloat(row.ca_ttc) || 0, trafic: row.trafic || 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sites = Object.keys(byDate).sort().map(s => {
|
||||||
|
const n = byDate[s]?.[targetDate] || { ca_ttc: 0, trafic: 0 };
|
||||||
|
const n1 = byDate[s]?.[n1DateStr] || { ca_ttc: 0, trafic: 0 };
|
||||||
|
const evolCa = n1.ca_ttc > 0 ? Math.round((n.ca_ttc / n1.ca_ttc - 1) * 1000) / 10 : null;
|
||||||
|
const evolTrafic = n1.trafic > 0 ? Math.round((n.trafic / n1.trafic - 1) * 1000) / 10 : null;
|
||||||
|
return {
|
||||||
|
site: s,
|
||||||
|
ca_ttc: n.ca_ttc, ca_ttc_n1: n1.ca_ttc,
|
||||||
|
evol_ca: evolCa !== null ? `${evolCa > 0 ? '+' : ''}${evolCa}%` : null,
|
||||||
|
trafic: n.trafic, trafic_n1: n1.trafic,
|
||||||
|
evol_trafic: evolTrafic !== null ? `${evolTrafic > 0 ? '+' : ''}${evolTrafic}%` : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
date: targetDate,
|
||||||
|
date_n1: n1DateStr,
|
||||||
|
sites,
|
||||||
|
top10_ca: top10CA.rows,
|
||||||
|
top10_qte: top10Qte.rows,
|
||||||
|
top10_marge: top10Marge.rows,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
+2
-1
@@ -4,7 +4,7 @@ const cron = require('node-cron');
|
|||||||
const { syncArticles } = require('./tables/articles');
|
const { syncArticles } = require('./tables/articles');
|
||||||
const { syncFournisseurs } = require('./tables/fournisseurs');
|
const { syncFournisseurs } = require('./tables/fournisseurs');
|
||||||
const { syncStock } = require('./tables/stock');
|
const { syncStock } = require('./tables/stock');
|
||||||
const { syncMouvements } = require('./tables/mouvements');
|
const { syncMouvements, syncStatOpCA } = require('./tables/mouvements');
|
||||||
const { syncCommandes } = require('./tables/commandes');
|
const { syncCommandes } = require('./tables/commandes');
|
||||||
const { syncReferentiel } = require('./tables/referentiel');
|
const { syncReferentiel } = require('./tables/referentiel');
|
||||||
const { syncRanking } = require('./tables/ranking');
|
const { syncRanking } = require('./tables/ranking');
|
||||||
@@ -24,6 +24,7 @@ const STEPS = [
|
|||||||
{ name: 'fournisseurs', fn: syncFournisseurs },
|
{ name: 'fournisseurs', fn: syncFournisseurs },
|
||||||
{ name: 'stock', fn: syncStock },
|
{ name: 'stock', fn: syncStock },
|
||||||
{ name: 'mouvements', fn: syncMouvements },
|
{ name: 'mouvements', fn: syncMouvements },
|
||||||
|
{ name: 'statopca', fn: syncStatOpCA },
|
||||||
{ name: 'commandes', fn: syncCommandes },
|
{ name: 'commandes', fn: syncCommandes },
|
||||||
{ name: 'ranking', fn: syncRanking },
|
{ name: 'ranking', fn: syncRanking },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
const { getMssql, getPg } = require('../db');
|
const { getMssql, getPg } = require('../db');
|
||||||
const { logSync } = require('../utils');
|
const { logSync } = require('../utils');
|
||||||
|
|
||||||
|
const STATOPCA_COLS = ['site', 'datmvt', 'mnt', 'nbticket'];
|
||||||
|
|
||||||
const MVTART_COLS = [
|
const MVTART_COLS = [
|
||||||
'artnoid','datmvt','site','libmvt','genremvt',
|
'artnoid','datmvt','site','libmvt','genremvt',
|
||||||
'qtemvt','valmvt','mntmvtht','mntmvtttc','margemvt',
|
'qtemvt','valmvt','mntmvtht','mntmvtttc','margemvt',
|
||||||
@@ -135,4 +137,52 @@ async function syncMouvements(force) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { syncMouvements };
|
// === StatOpCAJour (upsert par site+date) ===
|
||||||
|
async function syncStatOpCA(force) {
|
||||||
|
const ms = await getMssql();
|
||||||
|
const pg = getPg();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const maxRes = await pg.query(`SELECT MAX(datmvt) AS last FROM statopca`);
|
||||||
|
let lastDate = maxRes.rows[0]?.last;
|
||||||
|
if (!lastDate || force) {
|
||||||
|
const twoYearsAgo = new Date();
|
||||||
|
twoYearsAgo.setFullYear(twoYearsAgo.getFullYear() - 2);
|
||||||
|
lastDate = twoYearsAgo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ms.request()
|
||||||
|
.input('lastDate', lastDate)
|
||||||
|
.query(`SELECT SITE, DatMvt, mnt, nbTicket FROM StatOpCAJour WHERE DatMvt > @lastDate ORDER BY DatMvt`);
|
||||||
|
|
||||||
|
const rows = res.recordset.map(r => ({
|
||||||
|
site: r.SITE,
|
||||||
|
datmvt: r.DatMvt,
|
||||||
|
mnt: r.mnt,
|
||||||
|
nbticket: r.nbTicket,
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (let i = 0; i < rows.length; i += CHUNK) {
|
||||||
|
const chunk = rows.slice(i, i + CHUNK);
|
||||||
|
const values = [];
|
||||||
|
const placeholders = chunk.map((row, ri) => {
|
||||||
|
STATOPCA_COLS.forEach(col => values.push(row[col] ?? null));
|
||||||
|
const ph = STATOPCA_COLS.map((_, ci) => `$${ri * STATOPCA_COLS.length + ci + 1}`);
|
||||||
|
return `(${ph.join(', ')})`;
|
||||||
|
});
|
||||||
|
await pg.query(
|
||||||
|
`INSERT INTO statopca (${STATOPCA_COLS.join(', ')}) VALUES ${placeholders.join(', ')}
|
||||||
|
ON CONFLICT (site, datmvt) DO UPDATE SET mnt=EXCLUDED.mnt, nbticket=EXCLUDED.nbticket`,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await logSync(pg, 'statopca', rows.length, 'ok');
|
||||||
|
console.log(`[statopca] ${rows.length} lignes`);
|
||||||
|
} catch (err) {
|
||||||
|
await logSync(pg, 'statopca', 0, 'error', err.message);
|
||||||
|
console.error(`[statopca] ERREUR: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { syncMouvements, syncStatOpCA };
|
||||||
Reference in new issue
Block a user