feat: add ranking table sync and API endpoints
Sync Ranking table from SQL Server to PostgreSQL with ranking_ca, ranking_qte, ranking_mag_* metrics. Expose via /api/ranking and integrate into /api/articles/:id/referentiel response. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
8cafc196cf
commit
792ddc71d2
6 files changed
+174
No files matched your search
@@ -214,6 +214,35 @@ CREATE TABLE IF NOT EXISTS mvtreg (
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_mvtreg_datmvt ON mvtreg (datmvt);
|
CREATE INDEX IF NOT EXISTS idx_mvtreg_datmvt ON mvtreg (datmvt);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Ranking réseau / magasin
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ranking (
|
||||||
|
gencod TEXT,
|
||||||
|
site TEXT,
|
||||||
|
libelle TEXT,
|
||||||
|
foucentrale TEXT,
|
||||||
|
nomfoucentrale TEXT,
|
||||||
|
ranking_ca NUMERIC(10,2),
|
||||||
|
ranking_qte NUMERIC(10,2),
|
||||||
|
ranking_mag_ca NUMERIC(10,2),
|
||||||
|
ranking_mag_qte NUMERIC(10,2),
|
||||||
|
ranking_mag_marge NUMERIC(10,2),
|
||||||
|
pv_calcule NUMERIC(12,4),
|
||||||
|
pv_mag NUMERIC(12,4),
|
||||||
|
pv_cen NUMERIC(12,4),
|
||||||
|
codefamille TEXT,
|
||||||
|
libellefamille TEXT,
|
||||||
|
fichier TEXT,
|
||||||
|
date_maj TIMESTAMP,
|
||||||
|
date_integration TIMESTAMP,
|
||||||
|
date_calcul_mag TIMESTAMP,
|
||||||
|
PRIMARY KEY (gencod, site)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ranking_gencod ON ranking (gencod);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ranking_site ON ranking (site);
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- Commandes fournisseurs
|
-- Commandes fournisseurs
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const stockRouter = require('./routes/stock');
|
|||||||
const mouvementsRouter = require('./routes/mouvements');
|
const mouvementsRouter = require('./routes/mouvements');
|
||||||
const performanceRouter = require('./routes/performance');
|
const performanceRouter = require('./routes/performance');
|
||||||
const syncRouter = require('./routes/sync');
|
const syncRouter = require('./routes/sync');
|
||||||
|
const rankingRouter = require('./routes/ranking');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@@ -34,6 +35,7 @@ app.use('/api/stock', stockRouter);
|
|||||||
app.use('/api/mouvements', mouvementsRouter);
|
app.use('/api/mouvements', mouvementsRouter);
|
||||||
app.use('/api/performance', performanceRouter);
|
app.use('/api/performance', performanceRouter);
|
||||||
app.use('/api/sync', syncRouter);
|
app.use('/api/sync', syncRouter);
|
||||||
|
app.use('/api/ranking', rankingRouter);
|
||||||
|
|
||||||
// 404
|
// 404
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
|
|||||||
@@ -222,10 +222,27 @@ router.get('/:id/referentiel', async (req, res) => {
|
|||||||
notes = notesRes.rows;
|
notes = notesRes.rows;
|
||||||
} catch (e) { /* Table optionnelle */ }
|
} catch (e) { /* Table optionnelle */ }
|
||||||
|
|
||||||
|
let ranking = [];
|
||||||
|
try {
|
||||||
|
const rankRes = await pool.query(`
|
||||||
|
SELECT r.gencod, r.site, r.ranking_ca, r.ranking_qte,
|
||||||
|
r.ranking_mag_ca, r.ranking_mag_qte, r.ranking_mag_marge,
|
||||||
|
r.foucentrale, r.nomfoucentrale,
|
||||||
|
r.pv_calcule, r.pv_mag, r.pv_cen,
|
||||||
|
r.date_maj, r.date_calcul_mag
|
||||||
|
FROM ranking r
|
||||||
|
JOIN art_gtin g ON g.gtin = r.gencod
|
||||||
|
WHERE g.idarticle = $1
|
||||||
|
ORDER BY r.site
|
||||||
|
`, [id]);
|
||||||
|
ranking = rankRes.rows;
|
||||||
|
} catch (e) { /* Table optionnelle */ }
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
article: article.rows[0],
|
article: article.rows[0],
|
||||||
gtins: gtins.rows,
|
gtins: gtins.rows,
|
||||||
gammes: gammes.rows,
|
gammes: gammes.rows,
|
||||||
|
ranking,
|
||||||
stock: stock.rows,
|
stock: stock.rows,
|
||||||
stock_remballe: stockRemballe,
|
stock_remballe: stockRemballe,
|
||||||
prix: {
|
prix: {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { getPool } = require('../config/database');
|
||||||
|
|
||||||
|
// GET /api/ranking?gencod=&codein=&site=&foucentrale=&limit=
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const { gencod = '', codein = '', site = '', foucentrale = '', limit = 50 } = req.query;
|
||||||
|
const limitNum = Math.max(1, Math.min(parseInt(limit) || 50, 500));
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
r.gencod, r.site, r.libelle,
|
||||||
|
r.foucentrale, r.nomfoucentrale,
|
||||||
|
r.ranking_ca, r.ranking_qte,
|
||||||
|
r.ranking_mag_ca, r.ranking_mag_qte, r.ranking_mag_marge,
|
||||||
|
r.pv_calcule, r.pv_mag, r.pv_cen,
|
||||||
|
r.codefamille, r.libellefamille,
|
||||||
|
r.date_maj, r.date_calcul_mag,
|
||||||
|
a.no_id AS art_no_id, a.codein
|
||||||
|
FROM ranking r
|
||||||
|
LEFT JOIN art_gtin g ON g.gtin = r.gencod
|
||||||
|
LEFT JOIN articles a ON a.no_id = g.idarticle
|
||||||
|
WHERE ($1 = '' OR r.gencod = $1)
|
||||||
|
AND ($2 = '' OR a.codein = $2)
|
||||||
|
AND ($3 = '' OR r.site LIKE $3)
|
||||||
|
AND ($4 = '' OR r.foucentrale LIKE $4)
|
||||||
|
ORDER BY r.ranking_ca ASC NULLS LAST
|
||||||
|
LIMIT $5
|
||||||
|
`, [gencod, codein, `%${site}%`, `%${foucentrale}%`, limitNum]);
|
||||||
|
|
||||||
|
res.json({ count: result.rows.length, ranking: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/ranking/article/:id - Ranking pour un article (par no_id)
|
||||||
|
router.get('/article/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const pool = getPool();
|
||||||
|
const artNoId = req.params.id;
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
r.gencod, r.site, r.libelle,
|
||||||
|
r.foucentrale, r.nomfoucentrale,
|
||||||
|
r.ranking_ca, r.ranking_qte,
|
||||||
|
r.ranking_mag_ca, r.ranking_mag_qte, r.ranking_mag_marge,
|
||||||
|
r.pv_calcule, r.pv_mag, r.pv_cen,
|
||||||
|
r.codefamille, r.libellefamille,
|
||||||
|
r.date_maj, r.date_calcul_mag
|
||||||
|
FROM ranking r
|
||||||
|
JOIN art_gtin g ON g.gtin = r.gencod
|
||||||
|
WHERE g.idarticle = $1
|
||||||
|
ORDER BY r.site
|
||||||
|
`, [artNoId]);
|
||||||
|
|
||||||
|
res.json({ artNoId, ranking: result.rows });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -7,6 +7,7 @@ const { syncStock } = require('./tables/stock');
|
|||||||
const { syncMouvements } = require('./tables/mouvements');
|
const { syncMouvements } = 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 { closeAll } = require('./db');
|
const { closeAll } = require('./db');
|
||||||
const {
|
const {
|
||||||
startDashboard, registerSyncFn,
|
startDashboard, registerSyncFn,
|
||||||
@@ -24,6 +25,7 @@ const STEPS = [
|
|||||||
{ name: 'stock', fn: syncStock },
|
{ name: 'stock', fn: syncStock },
|
||||||
{ name: 'mouvements', fn: syncMouvements },
|
{ name: 'mouvements', fn: syncMouvements },
|
||||||
{ name: 'commandes', fn: syncCommandes },
|
{ name: 'commandes', fn: syncCommandes },
|
||||||
|
{ name: 'ranking', fn: syncRanking },
|
||||||
];
|
];
|
||||||
|
|
||||||
async function syncAll(force = false) {
|
async function syncAll(force = false) {
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
const { getMssql, getPg } = require('../db');
|
||||||
|
const { batchUpsert, logSync, safeStr, safeNum, safeDecimal } = require('../utils');
|
||||||
|
|
||||||
|
const COLS = [
|
||||||
|
'gencod', 'site', 'libelle', 'foucentrale', 'nomfoucentrale',
|
||||||
|
'ranking_ca', 'ranking_qte', 'ranking_mag_ca', 'ranking_mag_qte', 'ranking_mag_marge',
|
||||||
|
'pv_calcule', 'pv_mag', 'pv_cen',
|
||||||
|
'codefamille', 'libellefamille', 'fichier',
|
||||||
|
'date_maj', 'date_integration', 'date_calcul_mag',
|
||||||
|
];
|
||||||
|
|
||||||
|
async function syncRanking(force) {
|
||||||
|
const ms = await getMssql();
|
||||||
|
const pg = getPg();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await ms.request().query(`
|
||||||
|
SELECT GENCOD, SITE, LIBELLE, FOUCENTRALE, NOMFOUCENTRALE,
|
||||||
|
RANKING_CA, RANKING_QTE, RANKING_MAG_CA, RANKING_MAG_QTE, RANKING_MAG_MARGE,
|
||||||
|
PV_CALCULE, PV_MAG, PV_CEN,
|
||||||
|
CODEFAMILLE, LIBELLEFAMILLE, FICHIER,
|
||||||
|
DATE_MAJ, DATE_INTEGRATION, DATE_CALCUL_MAG
|
||||||
|
FROM Ranking
|
||||||
|
WHERE GENCOD IS NOT NULL
|
||||||
|
`);
|
||||||
|
|
||||||
|
const rows = res.recordset.map(r => ({
|
||||||
|
gencod: safeStr(r.GENCOD),
|
||||||
|
site: safeStr(r.SITE) || '000',
|
||||||
|
libelle: safeStr(r.LIBELLE),
|
||||||
|
foucentrale: safeStr(r.FOUCENTRALE),
|
||||||
|
nomfoucentrale: safeStr(r.NOMFOUCENTRALE),
|
||||||
|
ranking_ca: safeNum(r.RANKING_CA),
|
||||||
|
ranking_qte: safeNum(r.RANKING_QTE),
|
||||||
|
ranking_mag_ca: safeNum(r.RANKING_MAG_CA),
|
||||||
|
ranking_mag_qte: safeNum(r.RANKING_MAG_QTE),
|
||||||
|
ranking_mag_marge: safeNum(r.RANKING_MAG_MARGE),
|
||||||
|
pv_calcule: safeDecimal(r.PV_CALCULE),
|
||||||
|
pv_mag: safeDecimal(r.PV_MAG),
|
||||||
|
pv_cen: safeDecimal(r.PV_CEN),
|
||||||
|
codefamille: safeStr(r.CODEFAMILLE),
|
||||||
|
libellefamille: safeStr(r.LIBELLEFAMILLE),
|
||||||
|
fichier: safeStr(r.FICHIER),
|
||||||
|
date_maj: r.DATE_MAJ || null,
|
||||||
|
date_integration: r.DATE_INTEGRATION || null,
|
||||||
|
date_calcul_mag: r.DATE_CALCUL_MAG || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const count = await batchUpsert(pg, 'ranking', rows, ['gencod', 'site'], COLS);
|
||||||
|
await logSync(pg, 'ranking', count, 'ok');
|
||||||
|
console.log(`[ranking] ${count} lignes upsert`);
|
||||||
|
} catch (err) {
|
||||||
|
await logSync(pg, 'ranking', 0, 'error', err.message);
|
||||||
|
console.error(`[ranking] ERREUR: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { syncRanking };
|
||||||
Reference in new issue
Block a user