diff --git a/postgres/init.sql b/postgres/init.sql index 66ae679..0e2e60e 100644 --- a/postgres/init.sql +++ b/postgres/init.sql @@ -214,6 +214,35 @@ CREATE TABLE IF NOT EXISTS mvtreg ( ); 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 -- ============================================================ diff --git a/src/app.js b/src/app.js index 1e72a4e..c9d77d0 100644 --- a/src/app.js +++ b/src/app.js @@ -10,6 +10,7 @@ const stockRouter = require('./routes/stock'); const mouvementsRouter = require('./routes/mouvements'); const performanceRouter = require('./routes/performance'); const syncRouter = require('./routes/sync'); +const rankingRouter = require('./routes/ranking'); const app = express(); app.use(express.json()); @@ -34,6 +35,7 @@ app.use('/api/stock', stockRouter); app.use('/api/mouvements', mouvementsRouter); app.use('/api/performance', performanceRouter); app.use('/api/sync', syncRouter); +app.use('/api/ranking', rankingRouter); // 404 app.use((req, res) => { diff --git a/src/routes/articles.js b/src/routes/articles.js index 66596bf..9f9002a 100644 --- a/src/routes/articles.js +++ b/src/routes/articles.js @@ -222,10 +222,27 @@ router.get('/:id/referentiel', async (req, res) => { notes = notesRes.rows; } 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({ article: article.rows[0], gtins: gtins.rows, gammes: gammes.rows, + ranking, stock: stock.rows, stock_remballe: stockRemballe, prix: { diff --git a/src/routes/ranking.js b/src/routes/ranking.js new file mode 100644 index 0000000..8cf4a6c --- /dev/null +++ b/src/routes/ranking.js @@ -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; diff --git a/sync/index.js b/sync/index.js index 127961a..7c6ea79 100644 --- a/sync/index.js +++ b/sync/index.js @@ -7,6 +7,7 @@ const { syncStock } = require('./tables/stock'); const { syncMouvements } = require('./tables/mouvements'); const { syncCommandes } = require('./tables/commandes'); const { syncReferentiel } = require('./tables/referentiel'); +const { syncRanking } = require('./tables/ranking'); const { closeAll } = require('./db'); const { startDashboard, registerSyncFn, @@ -24,6 +25,7 @@ const STEPS = [ { name: 'stock', fn: syncStock }, { name: 'mouvements', fn: syncMouvements }, { name: 'commandes', fn: syncCommandes }, + { name: 'ranking', fn: syncRanking }, ]; async function syncAll(force = false) { diff --git a/sync/tables/ranking.js b/sync/tables/ranking.js new file mode 100644 index 0000000..acc2080 --- /dev/null +++ b/sync/tables/ranking.js @@ -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 };