Initial commit : API Node.js + PostgreSQL + sync service Docker
- API Express/pg connectée à PostgreSQL (converti depuis mssql) - 8 routes : articles, fournisseurs, commandes, stock, mouvements, performance, schema, sync - Service sync nightly (mssql → pg, cron 3h00) - Docker Compose : postgres + api + sync + frpc (tunnel sortant) - Schéma PostgreSQL 17 tables dans postgres/init.sql Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
d565ed9f10
28 files changed
+2423
No files matched your search
+74
@@ -0,0 +1,74 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const { getPool } = require('./config/database');
|
||||
|
||||
const schemaRouter = require('./routes/schema');
|
||||
const articlesRouter = require('./routes/articles');
|
||||
const fournisseursRouter = require('./routes/fournisseurs');
|
||||
const commandesRouter = require('./routes/commandes');
|
||||
const stockRouter = require('./routes/stock');
|
||||
const mouvementsRouter = require('./routes/mouvements');
|
||||
const performanceRouter = require('./routes/performance');
|
||||
const syncRouter = require('./routes/sync');
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
await pool.query('SELECT 1 AS ok');
|
||||
res.json({ status: 'ok', database: process.env.PG_DB, server: process.env.PG_HOST });
|
||||
} catch (err) {
|
||||
res.status(500).json({ status: 'error', detail: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Routes
|
||||
app.use('/api/schema', schemaRouter);
|
||||
app.use('/api/articles', articlesRouter);
|
||||
app.use('/api/fournisseurs', fournisseursRouter);
|
||||
app.use('/api/commandes', commandesRouter);
|
||||
app.use('/api/stock', stockRouter);
|
||||
app.use('/api/mouvements', mouvementsRouter);
|
||||
app.use('/api/performance', performanceRouter);
|
||||
app.use('/api/sync', syncRouter);
|
||||
|
||||
// 404
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ error: 'Route introuvable' });
|
||||
});
|
||||
|
||||
const PORT = process.env.API_PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`\nAPI Foirfouille démarrée sur http://localhost:${PORT}`);
|
||||
console.log(`Base : ${process.env.PG_HOST}/${process.env.PG_DB} (PostgreSQL)\n`);
|
||||
console.log('Endpoints disponibles :');
|
||||
console.log(' GET /api/health');
|
||||
console.log(' GET /api/schema/tables[?search=]');
|
||||
console.log(' GET /api/schema/top[?limit=]');
|
||||
console.log(' GET /api/schema/tables/:nom');
|
||||
console.log(' GET /api/articles[?search=&codein=&ean=&codefou=&actif=&page=&limit=]');
|
||||
console.log(' GET /api/articles/:id');
|
||||
console.log(' GET /api/articles/:id/referentiel');
|
||||
console.log(' GET /api/articles/:id/mouvements');
|
||||
console.log(' GET /api/fournisseurs[?search=]');
|
||||
console.log(' GET /api/fournisseurs/:code/articles');
|
||||
console.log(' GET /api/fournisseurs/:code/commandes');
|
||||
console.log(' GET /api/commandes[?dateDebut=&dateFin=&codefou=]');
|
||||
console.log(' GET /api/commandes/:noCommande');
|
||||
console.log(' GET /api/commandes/receptions/liste');
|
||||
console.log(' GET /api/stock[?site=]');
|
||||
console.log(' GET /api/stock/article/:id');
|
||||
console.log(' GET /api/stock/site/:site');
|
||||
console.log(' GET /api/stock/valorisation');
|
||||
console.log(' GET /api/mouvements/articles[?dateDebut=&dateFin=&site=]');
|
||||
console.log(' GET /api/mouvements/reglements[?dateDebut=&dateFin=]');
|
||||
console.log(' GET /api/mouvements/synthese[?dateDebut=&dateFin=&site=]');
|
||||
console.log(' GET /api/performance/ca[?dateDebut=&dateFin=&site=&groupBy=jour|mois]');
|
||||
console.log(' GET /api/performance/hitparade[?dateDebut=&dateFin=&site=&limit=&groupBy=qte|ca|marge]');
|
||||
console.log(' GET /api/performance/ca/nomenclature[?dateDebut=&dateFin=&site=&niveau=]');
|
||||
console.log(' GET /api/performance/ca/gamme[?dateDebut=&dateFin=&site=]');
|
||||
console.log(' GET /api/sync/status');
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
require('dotenv').config();
|
||||
const { Pool } = require('pg');
|
||||
|
||||
let pool = null;
|
||||
|
||||
function getPool() {
|
||||
if (!pool) {
|
||||
pool = new Pool({
|
||||
host: process.env.PG_HOST || 'postgres',
|
||||
port: parseInt(process.env.PG_PORT) || 5432,
|
||||
database: process.env.PG_DB || 'foirfouille',
|
||||
user: process.env.PG_USER || 'ff_api',
|
||||
password: process.env.PG_PASSWORD,
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
});
|
||||
pool.on('error', (err) => {
|
||||
console.error('PostgreSQL pool error:', err.message);
|
||||
});
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
module.exports = { getPool };
|
||||
@@ -0,0 +1,270 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getPool } = require('../config/database');
|
||||
|
||||
// GET /api/articles?search=&codein=&ean=&actif=&codefou=&page=&limit=
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { search = '', codein = '', ean = '', actif = '', codefou = '', page = 1, limit = 50 } = req.query;
|
||||
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||
const limitNum = Math.max(1, Math.min(parseInt(limit) || 50, 500));
|
||||
const offsetNum = (pageNum - 1) * limitNum;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
a.NO_ID, a.CODEIN, a.LIBELLE1, a.LIBELLE2, a.LIB_TICKET,
|
||||
a.TAX_CODE, a.ACH_CODE, a.UTILISABLE, a.ACTIF, a.SUSPENDU,
|
||||
a.SUIVIDATECREATION, a.SUIVIDATEMODIF,
|
||||
ai.PRIX_VENTE_MINI, ai.PRIX_VENTE_MAXI, ai.ECO_TTC,
|
||||
ai.ON_WEB, ai.INTERDIT_REMISE, ai.NOMPHOTO,
|
||||
ai.DATEDEBVENTE, ai.DATEFINVENTE,
|
||||
pa.PA,
|
||||
g.GTIN,
|
||||
fpref.CODE AS CODEFOU_PRINCIPAL,
|
||||
fpref.NOM_FOU AS NOM_FOU_PRINCIPAL,
|
||||
fpref.REF AS REF_FOU_PRINCIPALE,
|
||||
fpref.PCB AS PCB_PRINCIPAL
|
||||
FROM ARTICLES a
|
||||
LEFT JOIN ARTICLE_INFOSUP ai ON ai.ARTNOID = a.NO_ID
|
||||
LEFT JOIN Cube_PA pa ON pa.ArtNoId = a.NO_ID
|
||||
LEFT JOIN ART_GTIN g ON g.IDARTICLE = a.NO_ID AND g.PREFERENTIEL = 1
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT f.CODE, f.REFERENCE AS REF, f.QTEUA AS PCB,
|
||||
fa.RAISONSOCIALE AS NOM_FOU
|
||||
FROM ARTFOU1 f
|
||||
LEFT JOIN FOUADR1 fa ON fa.CODE = f.CODE AND fa.SIT_CODE = '000'
|
||||
WHERE f.ART_NO_ID = a.NO_ID
|
||||
ORDER BY f.PREFERENCE
|
||||
LIMIT 1
|
||||
) fpref ON true
|
||||
WHERE
|
||||
($1 = '%%' OR a.LIBELLE1 LIKE $1 OR a.LIBELLE2 LIKE $1)
|
||||
AND ($2 = '%%' OR a.CODEIN LIKE $2)
|
||||
AND ($3 = '%%' OR g.GTIN LIKE $3)
|
||||
AND ($5 = '' OR (
|
||||
$5 = '1' AND a.ACTIF IS NOT NULL AND a.SUSPENDU IS NULL
|
||||
) OR (
|
||||
$5 = '0' AND a.SUSPENDU IS NOT NULL
|
||||
))
|
||||
AND ($4 = '%%' OR EXISTS (
|
||||
SELECT 1 FROM ARTFOU1 f WHERE f.ART_NO_ID = a.NO_ID AND f.CODE LIKE $4
|
||||
))
|
||||
ORDER BY a.LIBELLE1
|
||||
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||
`, [`%${search}%`, `%${codein}%`, `%${ean}%`, `%${codefou}%`, actif]);
|
||||
|
||||
res.json({ page: pageNum, limit: limitNum, articles: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/articles/:id - Détail complet d'un article
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
|
||||
const article = await pool.query(`
|
||||
SELECT
|
||||
a.*, ai.PRIX_VENTE_MINI, ai.PRIX_VENTE_MAXI,
|
||||
ai.ECO_HT, ai.ECO_TTC, ai.ON_WEB, ai.INTERDIT_REMISE,
|
||||
ai.NOMPHOTO, ai.DATEDEBVENTE, ai.DATEFINVENTE,
|
||||
ai.DATEVALIDITEACHAT, ai.COMMENTAIRE, ai.PV_CONSEILLE,
|
||||
ai.FRAISLOGISTIC, ai.TRANSIT, ai.DISTRIBUTION,
|
||||
pa.PA
|
||||
FROM ARTICLES a
|
||||
LEFT JOIN ARTICLE_INFOSUP ai ON ai.ARTNOID = a.NO_ID
|
||||
LEFT JOIN Cube_PA pa ON pa.ArtNoId = a.NO_ID
|
||||
WHERE a.NO_ID = $1
|
||||
`, [req.params.id]);
|
||||
|
||||
if (!article.rows.length)
|
||||
return res.status(404).json({ error: 'Article introuvable' });
|
||||
|
||||
const gtins = await pool.query(
|
||||
`SELECT GTIN, PREFERENTIEL FROM ART_GTIN WHERE IDARTICLE = $1 ORDER BY PREFERENTIEL DESC`,
|
||||
[req.params.id]
|
||||
);
|
||||
|
||||
const stock = await pool.query(`
|
||||
SELECT Site, QTE, Prmp, ValStock, PV, StockDispo,
|
||||
DerniereVente, DerniereReception, StockMort
|
||||
FROM Cube_Stock WHERE ArtNoId = $1 ORDER BY Site
|
||||
`, [req.params.id]);
|
||||
|
||||
const prix = await pool.query(
|
||||
`SELECT Site, PV FROM Cube_PV WHERE ArtNoId = $1 ORDER BY Site`,
|
||||
[req.params.id]
|
||||
);
|
||||
|
||||
const fournisseurs = await pool.query(`
|
||||
SELECT f.CODE AS CODEFOU, fa.RAISONSOCIALE AS NOM_FOU,
|
||||
f.REFERENCE, f.EAN13, f.ITF,
|
||||
f.QTEUA, f.PCB, f.SPCB, f.DELAI, f.SECURITE, f.PREFERENCE, f.SUSPENDU,
|
||||
t.PRIXACHAT, t.REMISE_PROMOTION
|
||||
FROM ARTFOU1 f
|
||||
LEFT JOIN FOUADR1 fa ON fa.CODE = f.CODE AND fa.SIT_CODE = '000'
|
||||
LEFT JOIN ARTFOU2 t ON t.IDARTFOU1 = f.NO_ID
|
||||
WHERE f.ART_NO_ID = $1
|
||||
ORDER BY f.PREFERENCE
|
||||
`, [req.params.id]);
|
||||
|
||||
res.json({
|
||||
article: article.rows[0],
|
||||
gtins: gtins.rows,
|
||||
stock: stock.rows,
|
||||
prix: prix.rows,
|
||||
fournisseurs: fournisseurs.rows,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/articles/:id/referentiel - Fiche complète
|
||||
router.get('/:id/referentiel', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const id = req.params.id;
|
||||
|
||||
const article = await pool.query(`
|
||||
SELECT
|
||||
a.NO_ID, a.CODEIN, a.LIBELLE1, a.LIBELLE2, a.LIB_TICKET,
|
||||
a.TAX_CODE, a.ACTIF, a.SUSPENDU, a.UTILISABLE,
|
||||
a.SUIVIDATECREATION, a.SUIVIDATEMODIF,
|
||||
ai.PRIX_VENTE_MINI, ai.PRIX_VENTE_MAXI,
|
||||
ai.ECO_HT, ai.ECO_TTC, ai.ON_WEB,
|
||||
ai.INTERDIT_REMISE, ai.NOMPHOTO,
|
||||
ai.DATEDEBVENTE, ai.DATEFINVENTE,
|
||||
ai.PV_CONSEILLE, ai.FRAISLOGISTIC,
|
||||
ai.COMMENTAIRE,
|
||||
n.CODE AS NOM_CODE, n.LIBELLE AS NOM_LIBELLE, n.NIVEAU AS NOM_NIVEAU,
|
||||
n.CHEMIN_PERE AS NOM_CHEMIN_PERE
|
||||
FROM ARTICLES a
|
||||
LEFT JOIN ARTICLE_INFOSUP ai ON ai.ARTNOID = a.NO_ID
|
||||
LEFT JOIN NOMENCLATURE n ON n.NO_ID = a.NOM_NO_ID
|
||||
WHERE a.NO_ID = $1
|
||||
`, [id]);
|
||||
|
||||
if (!article.rows.length)
|
||||
return res.status(404).json({ error: 'Article introuvable' });
|
||||
|
||||
const gtins = await pool.query(
|
||||
`SELECT GTIN, PREFERENTIEL FROM ART_GTIN WHERE IDARTICLE = $1 ORDER BY PREFERENTIEL DESC`,
|
||||
[id]
|
||||
);
|
||||
|
||||
const gammes = await pool.query(`
|
||||
SELECT g.CODE AS GAMME_CODE, g.LIBELLE AS GAMME_LIBELLE,
|
||||
s.CODE AS SAISON_CODE, s.LIBELLE AS SAISON_LIBELLE,
|
||||
ags.IDGAMME, ags.IDSAISON
|
||||
FROM ART_GAMME_SAISON ags
|
||||
JOIN GAMMES g ON g.NO_ID = ags.IDGAMME
|
||||
LEFT JOIN SAISONS s ON s.NO_ID = ags.IDSAISON
|
||||
WHERE ags.ARTNOID = $1
|
||||
`, [id]);
|
||||
|
||||
const stock = await pool.query(`
|
||||
SELECT Site, QTE, Prmp, ValStock, PV, StockDispo,
|
||||
DerniereVente, DerniereReception, StockMort
|
||||
FROM Cube_Stock WHERE ArtNoId = $1 ORDER BY Site
|
||||
`, [id]);
|
||||
|
||||
const prixAchat = await pool.query(
|
||||
`SELECT ArtNoId, PA FROM Cube_PA WHERE ArtNoId = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
const prixVente = await pool.query(
|
||||
`SELECT Site, PV FROM Cube_PV WHERE ArtNoId = $1 ORDER BY Site`,
|
||||
[id]
|
||||
);
|
||||
|
||||
const fournisseurs = await pool.query(`
|
||||
SELECT f.CODE AS CODEFOU, fa.RAISONSOCIALE AS NOM_FOU,
|
||||
f.REFERENCE AS REF_FOU, f.EAN13, f.ITF,
|
||||
f.QTEUA AS PCB, f.SPCB, f.DELAI, f.SECURITE,
|
||||
f.PREFERENCE, f.SUSPENDU,
|
||||
t.PRIXACHAT, t.REMISE_PROMOTION
|
||||
FROM ARTFOU1 f
|
||||
LEFT JOIN FOUADR1 fa ON fa.CODE = f.CODE AND fa.SIT_CODE = '000'
|
||||
LEFT JOIN ARTFOU2 t ON t.IDARTFOU1 = f.NO_ID
|
||||
WHERE f.ART_NO_ID = $1
|
||||
ORDER BY f.PREFERENCE
|
||||
`, [id]);
|
||||
|
||||
const mvtAgreges = await pool.query(`
|
||||
SELECT
|
||||
MAX(CASE WHEN GenreMvt = 1 THEN DatMvt END) AS DERNIERE_ENTREE,
|
||||
MAX(CASE WHEN GenreMvt = 3 THEN DatMvt END) AS DERNIERE_VENTE,
|
||||
SUM(CASE WHEN GenreMvt = 3 THEN QteMvt ELSE 0 END) AS QTE_TOTALE_VENDUE,
|
||||
SUM(CASE WHEN GenreMvt = 3 THEN MntMvtTTC ELSE 0 END) AS CA_TTC_TOTAL,
|
||||
SUM(CASE WHEN GenreMvt = 3 THEN MargeMvt ELSE 0 END) AS MARGE_TOTALE
|
||||
FROM MvtArt WHERE ArtNoId = $1
|
||||
`, [id]);
|
||||
|
||||
let stockRemballe = [];
|
||||
try {
|
||||
const remb = await pool.query(
|
||||
`SELECT Site, QTE AS QTE_REMBALLE, DateSaisie FROM InvRemballe WHERE ArtNoId = $1 ORDER BY Site`,
|
||||
[id]
|
||||
);
|
||||
stockRemballe = remb.rows;
|
||||
} catch (e) { /* Table optionnelle */ }
|
||||
|
||||
let notes = [];
|
||||
try {
|
||||
const notesRes = await pool.query(
|
||||
`SELECT Note, DateSaisie, Auteur FROM NoteArticles WHERE ArtNoId = $1 ORDER BY DateSaisie DESC`,
|
||||
[id]
|
||||
);
|
||||
notes = notesRes.rows;
|
||||
} catch (e) { /* Table optionnelle */ }
|
||||
|
||||
res.json({
|
||||
article: article.rows[0],
|
||||
gtins: gtins.rows,
|
||||
gammes: gammes.rows,
|
||||
stock: stock.rows,
|
||||
stock_remballe: stockRemballe,
|
||||
prix: {
|
||||
achat: prixAchat.rows[0]?.pa ?? null,
|
||||
vente_par_site: prixVente.rows,
|
||||
},
|
||||
fournisseurs: fournisseurs.rows,
|
||||
performance: mvtAgreges.rows[0],
|
||||
notes,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/articles/:id/mouvements?dateDebut=&dateFin=&site=&page=&limit=
|
||||
router.get('/:id/mouvements', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { dateDebut = '', dateFin = '', site = '', page = 1, limit = 100 } = req.query;
|
||||
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||
const offsetNum = (pageNum - 1) * limitNum;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT DatMvt, Site, LibMvt, GenreMvt, QteMvt, ValMvt,
|
||||
MntMvtHt, MntMvtTTC, MargeMvt, QteStock, Prmp, CODEFOU
|
||||
FROM MvtArt
|
||||
WHERE ArtNoId = $1
|
||||
AND DatMvt BETWEEN $2 AND $3
|
||||
AND Site LIKE $4
|
||||
ORDER BY DatMvt DESC
|
||||
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||
`, [req.params.id, dateDebut || '2000-01-01', dateFin || '2099-12-31', `%${site}%`]);
|
||||
|
||||
res.json({ page: pageNum, limit: limitNum, mouvements: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,137 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getPool } = require('../config/database');
|
||||
|
||||
// GET /api/commandes?dateDebut=&dateFin=&codefou=&page=&limit=
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { dateDebut = '2020-01-01', dateFin = '2099-12-31', codefou = '', page = 1, limit = 100 } = req.query;
|
||||
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||
const offsetNum = (pageNum - 1) * limitNum;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
cv.CDEFOU_LIGNE_COM_NO_ID AS NO_COMMANDE,
|
||||
f.CODE AS CODEFOU,
|
||||
cv.ARTICLES_CODEIN AS CODEIN,
|
||||
cv.ARTICLES_LIBELLE1 AS LIBELLE,
|
||||
cv.ARTFOU1_REFERENCE AS REF_FOU,
|
||||
cv.ARTFOU1_EAN13 AS EAN13,
|
||||
cv.ARTFOU1_ITF AS ITF,
|
||||
cv.CDEFOU_LIGNE_QTECDE AS QTE_CDE,
|
||||
cv.CDEFOU_LIGNE_PRIXBRUT AS PRIX_BRUT,
|
||||
cv.CDEFOU_LIGNE_REMISE AS REMISE,
|
||||
cv.CDEFOU_LIGNE_PRIXNET AS PRIX_NET,
|
||||
cv.CDEFOU_LIGNE_MONTANT AS MONTANT,
|
||||
cv.CDEFOU_LIGNE_PRIXVENTE AS PRIX_VENTE,
|
||||
cv.CDEFOU_LIGNE_QTEACC AS QTE_ACCEPTEE,
|
||||
cv.CDEFOU_LIGNE_QTEANN AS QTE_ANNULEE,
|
||||
cv.CDEFOU_LIGNE_QTEATT AS QTE_ATTENTE,
|
||||
cv.CDEFOU_LIGNE_QTEREL AS QTE_RELIQUAT,
|
||||
cv.CDEFOU_LIGNE_CDELIGTARD AS DATE_LIVRAISON_CIBLE,
|
||||
cv.CDEFOU_LIGNE_CDELIGTOT AS DATE_LIVRAISON_TOT,
|
||||
cv.COMMENTAIRE,
|
||||
cv.SUIVIDATECREATION AS DATE_COMMANDE
|
||||
FROM CDEFOU_VIVANT cv
|
||||
JOIN ARTFOU1 f ON f.NO_ID = cv.ARTFOU1_NO_ID
|
||||
WHERE cv.SUIVIDATECREATION BETWEEN $1 AND $2
|
||||
AND f.CODE LIKE $3
|
||||
ORDER BY cv.SUIVIDATECREATION DESC
|
||||
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||
`, [dateDebut, dateFin, `%${codefou}%`]);
|
||||
|
||||
res.json({ page: pageNum, limit: limitNum, commandes: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/commandes/:noCommande - Détail d'une commande
|
||||
router.get('/:noCommande', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
cv.CDEFOU_LIGNE_COM_NO_ID AS NO_COMMANDE,
|
||||
f.CODE AS CODEFOU,
|
||||
cv.ARTICLES_CODEIN AS CODEIN,
|
||||
cv.ARTICLES_LIBELLE1 AS LIBELLE,
|
||||
cv.ARTICLES_LIBELLE2 AS LIBELLE2,
|
||||
cv.ARTFOU1_REFERENCE AS REF_FOU,
|
||||
cv.ARTFOU1_EAN13 AS EAN13,
|
||||
cv.ARTFOU1_ITF AS ITF,
|
||||
cv.CDEFOU_LIGNE_QTECDE AS QTE_CDE,
|
||||
cv.CDEFOU_LIGNE_PRIXBRUT AS PRIX_BRUT,
|
||||
cv.CDEFOU_LIGNE_REMISE AS REMISE,
|
||||
cv.CDEFOU_LIGNE_REMISE2 AS REMISE2,
|
||||
cv.CDEFOU_LIGNE_REMISE3 AS REMISE3,
|
||||
cv.CDEFOU_LIGNE_PRIXNET AS PRIX_NET,
|
||||
cv.CDEFOU_LIGNE_MONTANT AS MONTANT,
|
||||
cv.CDEFOU_LIGNE_PRIXVENTE AS PRIX_VENTE,
|
||||
cv.CDEFOU_LIGNE_GRATUIT AS QTE_GRATUITE,
|
||||
cv.CDEFOU_LIGNE_QTEACC AS QTE_ACCEPTEE,
|
||||
cv.CDEFOU_LIGNE_QTEANN AS QTE_ANNULEE,
|
||||
cv.CDEFOU_LIGNE_QTEATT AS QTE_ATTENTE,
|
||||
cv.CDEFOU_LIGNE_QTEREL AS QTE_RELIQUAT,
|
||||
cv.CDEFOU_LIGNE_CDELIGTARD AS DATE_LIVRAISON_CIBLE,
|
||||
cv.COTISATION_LOGISTIQUE,
|
||||
cv.FRAISLOGISTIC, cv.TRANSIT, cv.DISTRIBUTION, cv.TAXE,
|
||||
cv.COMMENTAIRE,
|
||||
cv.SUIVIDATECREATION AS DATE_COMMANDE
|
||||
FROM CDEFOU_VIVANT cv
|
||||
JOIN ARTFOU1 f ON f.NO_ID = cv.ARTFOU1_NO_ID
|
||||
WHERE cv.CDEFOU_LIGNE_COM_NO_ID = $1
|
||||
ORDER BY cv.ARTICLES_LIBELLE1
|
||||
`, [req.params.noCommande]);
|
||||
|
||||
if (!result.rows.length)
|
||||
return res.status(404).json({ error: 'Commande introuvable' });
|
||||
|
||||
res.json({
|
||||
noCommande: req.params.noCommande,
|
||||
nbLignes: result.rows.length,
|
||||
montantTotal: result.rows.reduce((s, l) => s + (l.montant || 0), 0),
|
||||
lignes: result.rows,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/commandes/receptions/liste?dateDebut=&dateFin=&page=&limit=
|
||||
router.get('/receptions/liste', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { dateDebut = '2020-01-01', dateFin = '2099-12-31', page = 1, limit = 100 } = req.query;
|
||||
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||
const offsetNum = (pageNum - 1) * limitNum;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
rl.NO_ID, rl.CDEFOU_RECEPTION_NO_ID,
|
||||
rl.QTEBL, rl.QTEREC, rl.QTEACC, rl.QTEREF, rl.QTEAFF,
|
||||
rl.PRIBRUT, rl.REMISE, rl.PRIREC, rl.MNTREC,
|
||||
rl.MOTIFREFUS, rl.RECPB, rl.RECPBOK,
|
||||
f.CODE AS CODEFOU,
|
||||
f.REFERENCE AS REF_FOU, f.EAN13,
|
||||
a.CODEIN, a.LIBELLE1,
|
||||
rl.SUIVIDATECREATION AS DATE_RECEPTION
|
||||
FROM CDEFOU_RECEPLIG rl
|
||||
JOIN ARTFOU1 f ON f.NO_ID = rl.ARTFOU1_NO_ID
|
||||
JOIN ARTICLES a ON a.NO_ID = f.ART_NO_ID
|
||||
WHERE rl.SUIVIDATECREATION BETWEEN $1 AND $2
|
||||
ORDER BY rl.SUIVIDATECREATION DESC
|
||||
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||
`, [dateDebut, dateFin]);
|
||||
|
||||
res.json({ page: pageNum, limit: limitNum, receptions: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,109 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getPool } = require('../config/database');
|
||||
|
||||
// GET /api/fournisseurs - Liste des fournisseurs distincts
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { search = '' } = req.query;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
f.CODE AS CODEFOU,
|
||||
MAX(fa.RAISONSOCIALE) AS NOM,
|
||||
MAX(fa.ADRLIGNE1) AS ADRESSE,
|
||||
MAX(fa.TELEPHONE) AS TELEPHONE,
|
||||
MAX(fa.EMAIL) AS EMAIL,
|
||||
COUNT(DISTINCT f.ART_NO_ID) AS NB_ARTICLES,
|
||||
MIN(f.SUIVIDATECREATION) AS PREMIERE_ENTREE,
|
||||
MAX(f.SUIVIDATEMODIF) AS DERNIERE_MODIF,
|
||||
SUM(CASE WHEN f.SUSPENDU IS NULL THEN 1 ELSE 0 END) AS NB_ACTIFS,
|
||||
SUM(CASE WHEN f.SUSPENDU IS NOT NULL THEN 1 ELSE 0 END) AS NB_SUSPENDUS
|
||||
FROM ARTFOU1 f
|
||||
LEFT JOIN FOUADR1 fa ON fa.CODE = f.CODE AND fa.SIT_CODE = '000'
|
||||
WHERE f.CODE LIKE $1
|
||||
GROUP BY f.CODE
|
||||
ORDER BY NB_ARTICLES DESC
|
||||
`, [`%${search}%`]);
|
||||
|
||||
res.json({ count: result.rows.length, fournisseurs: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/fournisseurs/:code/articles?page=&limit=&suspendu=
|
||||
router.get('/:code/articles', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { page = 1, limit = 50 } = req.query;
|
||||
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||
const limitNum = Math.max(1, Math.min(parseInt(limit) || 50, 500));
|
||||
const offsetNum = (pageNum - 1) * limitNum;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
a.NO_ID, a.CODEIN, a.LIBELLE1, a.LIBELLE2,
|
||||
f.REFERENCE AS REF_FOU, f.EAN13, f.ITF,
|
||||
f.PCB, f.SPCB, f.DELAI, f.SECURITE, f.SUSPENDU, f.PREFERENCE,
|
||||
t2.PRIXACHAT, t2.REMISE_PROMOTION,
|
||||
pa.PA,
|
||||
pv.PV AS PV_CENTRAL
|
||||
FROM ARTFOU1 f
|
||||
JOIN ARTICLES a ON a.NO_ID = f.ART_NO_ID
|
||||
LEFT JOIN ARTFOU2 t2 ON t2.IDARTFOU1 = f.NO_ID
|
||||
LEFT JOIN Cube_PA pa ON pa.ArtNoId = a.NO_ID
|
||||
LEFT JOIN Cube_PV pv ON pv.ArtNoId = a.NO_ID AND pv.Site = '000'
|
||||
WHERE f.CODE = $1
|
||||
ORDER BY a.LIBELLE1
|
||||
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||
`, [req.params.code]);
|
||||
|
||||
res.json({ code: req.params.code, page: pageNum, limit: limitNum, articles: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/fournisseurs/:code/commandes?dateDebut=&dateFin=&page=&limit=
|
||||
router.get('/:code/commandes', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { dateDebut = '2020-01-01', dateFin = '2099-12-31', page = 1, limit = 100 } = req.query;
|
||||
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||
const offsetNum = (pageNum - 1) * limitNum;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
cv.CDEFOU_LIGNE_COM_NO_ID AS NO_COMMANDE,
|
||||
cv.ARTICLES_CODEIN AS CODEIN,
|
||||
cv.ARTICLES_LIBELLE1 AS LIBELLE,
|
||||
cv.ARTFOU1_REFERENCE AS REF_FOU,
|
||||
cv.ARTFOU1_EAN13 AS EAN13,
|
||||
cv.CDEFOU_LIGNE_QTECDE AS QTE_CDE,
|
||||
cv.CDEFOU_LIGNE_PRIXBRUT AS PRIX_BRUT,
|
||||
cv.CDEFOU_LIGNE_REMISE AS REMISE,
|
||||
cv.CDEFOU_LIGNE_PRIXNET AS PRIX_NET,
|
||||
cv.CDEFOU_LIGNE_MONTANT AS MONTANT,
|
||||
cv.CDEFOU_LIGNE_QTEACC AS QTE_ACCEPTEE,
|
||||
cv.CDEFOU_LIGNE_QTEANN AS QTE_ANNULEE,
|
||||
cv.CDEFOU_LIGNE_QTEATT AS QTE_ATTENTE,
|
||||
cv.CDEFOU_LIGNE_CDELIGTARD AS DATE_LIVRAISON,
|
||||
cv.SUIVIDATECREATION AS DATE_COMMANDE
|
||||
FROM CDEFOU_VIVANT cv
|
||||
JOIN ARTFOU1 f ON f.NO_ID = cv.ARTFOU1_NO_ID
|
||||
WHERE f.CODE = $1
|
||||
AND cv.SUIVIDATECREATION BETWEEN $2 AND $3
|
||||
ORDER BY cv.SUIVIDATECREATION DESC
|
||||
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||
`, [req.params.code, dateDebut, dateFin]);
|
||||
|
||||
res.json({ code: req.params.code, page: pageNum, limit: limitNum, commandes: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,91 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getPool } = require('../config/database');
|
||||
|
||||
// GET /api/mouvements/articles?dateDebut=&dateFin=&site=&codefou=&page=&limit=
|
||||
router.get('/articles', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '', codefou = '', page = 1, limit = 100 } = req.query;
|
||||
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||
const offsetNum = (pageNum - 1) * limitNum;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
m.DatMvt, m.Site,
|
||||
a.CODEIN, a.LIBELLE1,
|
||||
m.LibMvt, m.GenreMvt, m.QteMvt, m.ValMvt,
|
||||
m.MntMvtHt, m.MntMvtTTC, m.MargeMvt,
|
||||
m.QteStock, m.Prmp, m.ValStock, m.CODEFOU
|
||||
FROM MvtArt m
|
||||
JOIN ARTICLES a ON a.NO_ID = m.ArtNoId
|
||||
WHERE m.DatMvt BETWEEN $1 AND $2
|
||||
AND m.Site LIKE $3
|
||||
AND m.CODEFOU LIKE $4
|
||||
ORDER BY m.DatMvt DESC
|
||||
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||
`, [dateDebut, dateFin, `%${site}%`, `%${codefou}%`]);
|
||||
|
||||
res.json({ page: pageNum, limit: limitNum, mouvements: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/mouvements/reglements?dateDebut=&dateFin=&page=&limit=
|
||||
router.get('/reglements', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', page = 1, limit = 100 } = req.query;
|
||||
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||
const offsetNum = (pageNum - 1) * limitNum;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
DatMvt, CodTick, CodCarteCli, CodDev,
|
||||
MntReg, MntRegDev, ClientNom, Echeance,
|
||||
REFERENCE, TYPEREG, SuiviDateCreation
|
||||
FROM MvtReg
|
||||
WHERE DatMvt BETWEEN $1 AND $2
|
||||
ORDER BY DatMvt DESC
|
||||
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||
`, [dateDebut, dateFin]);
|
||||
|
||||
res.json({ page: pageNum, limit: limitNum, reglements: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/mouvements/synthese?dateDebut=&dateFin=&site= - Synthèse par jour/site
|
||||
router.get('/synthese', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '' } = req.query;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
DatMvt::DATE AS Jour,
|
||||
Site,
|
||||
GenreMvt,
|
||||
COUNT(*) AS NB_MVT,
|
||||
SUM(QteMvt) AS QTE_TOTALE,
|
||||
SUM(MntMvtHt) AS MNT_HT,
|
||||
SUM(MntMvtTTC) AS MNT_TTC,
|
||||
SUM(MargeMvt) AS MARGE
|
||||
FROM MvtArt
|
||||
WHERE DatMvt BETWEEN $1 AND $2
|
||||
AND Site LIKE $3
|
||||
GROUP BY DatMvt::DATE, Site, GenreMvt
|
||||
ORDER BY Jour DESC, Site
|
||||
`, [dateDebut, dateFin, `%${site}%`]);
|
||||
|
||||
res.json({ synthese: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,156 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getPool } = require('../config/database');
|
||||
|
||||
// GET /api/performance/ca?dateDebut=&dateFin=&site=&groupBy=jour|mois
|
||||
router.get('/ca', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '', groupBy = 'jour' } = req.query;
|
||||
|
||||
const groupExpr = groupBy === 'mois'
|
||||
? "TO_CHAR(DatMvt, 'YYYY-MM')"
|
||||
: "DatMvt::DATE";
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
${groupExpr} AS Periode,
|
||||
Site,
|
||||
COUNT(*) AS NB_VENTES,
|
||||
SUM(QteMvt) AS QTE_VENDUE,
|
||||
SUM(MntMvtHt) AS CA_HT,
|
||||
SUM(MntMvtTTC) AS CA_TTC,
|
||||
SUM(MargeMvt) AS MARGE,
|
||||
CASE WHEN SUM(MntMvtTTC) > 0
|
||||
THEN ROUND(SUM(MargeMvt) / SUM(MntMvtTTC) * 100, 2)
|
||||
ELSE 0 END AS TAUX_MARGE
|
||||
FROM MvtArt
|
||||
WHERE GenreMvt = 3
|
||||
AND DatMvt BETWEEN $1 AND $2
|
||||
AND Site LIKE $3
|
||||
GROUP BY ${groupExpr}, Site
|
||||
ORDER BY Periode DESC, Site
|
||||
`, [dateDebut, dateFin, `%${site}%`]);
|
||||
|
||||
res.json({
|
||||
groupBy,
|
||||
dateDebut,
|
||||
dateFin,
|
||||
site: site || 'tous',
|
||||
ca: result.rows,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/performance/hitparade?dateDebut=&dateFin=&site=&limit=&groupBy=qte|ca|marge
|
||||
router.get('/hitparade', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '', limit = 50, groupBy = 'ca' } = req.query;
|
||||
const limitNum = Math.max(1, Math.min(parseInt(limit) || 50, 500));
|
||||
|
||||
const orderCol = groupBy === 'qte' ? 'QTE_VENDUE' : groupBy === 'marge' ? 'MARGE' : 'CA_TTC';
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
a.CODEIN,
|
||||
a.LIBELLE1,
|
||||
m.Site,
|
||||
COUNT(*) AS NB_PASSAGES,
|
||||
SUM(m.QteMvt) AS QTE_VENDUE,
|
||||
SUM(m.MntMvtHt) AS CA_HT,
|
||||
SUM(m.MntMvtTTC) AS CA_TTC,
|
||||
SUM(m.MargeMvt) AS MARGE,
|
||||
CASE WHEN SUM(m.MntMvtTTC) > 0
|
||||
THEN ROUND(SUM(m.MargeMvt) / SUM(m.MntMvtTTC) * 100, 2)
|
||||
ELSE 0 END AS TAUX_MARGE,
|
||||
MAX(m.DatMvt) AS DERNIERE_VENTE
|
||||
FROM MvtArt m
|
||||
JOIN ARTICLES a ON a.NO_ID = m.ArtNoId
|
||||
WHERE m.GenreMvt = 3
|
||||
AND m.DatMvt BETWEEN $1 AND $2
|
||||
AND m.Site LIKE $3
|
||||
GROUP BY a.CODEIN, a.LIBELLE1, m.Site
|
||||
ORDER BY ${orderCol} DESC
|
||||
LIMIT ${limitNum}
|
||||
`, [dateDebut, dateFin, `%${site}%`]);
|
||||
|
||||
res.json({
|
||||
dateDebut,
|
||||
dateFin,
|
||||
site: site || 'tous',
|
||||
classementPar: groupBy,
|
||||
hitparade: result.rows,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/performance/ca/nomenclature?dateDebut=&dateFin=&site=&niveau=
|
||||
router.get('/ca/nomenclature', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '', niveau = 1 } = req.query;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
n.CODE AS CODE_NOMEN,
|
||||
n.LIBELLE,
|
||||
n.NIVEAU,
|
||||
COUNT(DISTINCT m.ArtNoId) AS NB_ARTICLES,
|
||||
SUM(m.QteMvt) AS QTE_VENDUE,
|
||||
SUM(m.MntMvtHt) AS CA_HT,
|
||||
SUM(m.MntMvtTTC) AS CA_TTC,
|
||||
SUM(m.MargeMvt) AS MARGE
|
||||
FROM MvtArt m
|
||||
JOIN ARTICLES a ON a.NO_ID = m.ArtNoId
|
||||
JOIN NOMENCLATURE n ON n.NO_ID = a.NOM_NO_ID
|
||||
WHERE m.GenreMvt = 3
|
||||
AND m.DatMvt BETWEEN $1 AND $2
|
||||
AND m.Site LIKE $3
|
||||
AND n.NIVEAU = $4
|
||||
GROUP BY n.CODE, n.LIBELLE, n.NIVEAU
|
||||
ORDER BY CA_TTC DESC
|
||||
`, [dateDebut, dateFin, `%${site}%`, parseInt(niveau)]);
|
||||
|
||||
res.json({ dateDebut, dateFin, niveau: parseInt(niveau), ca_par_nomenclature: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/performance/ca/gamme?dateDebut=&dateFin=&site=
|
||||
router.get('/ca/gamme', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { dateDebut = '2024-01-01', dateFin = '2099-12-31', site = '' } = req.query;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
g.CODE AS GAMME,
|
||||
g.LIBELLE AS LIBELLE_GAMME,
|
||||
COUNT(DISTINCT m.ArtNoId) AS NB_ARTICLES,
|
||||
SUM(m.QteMvt) AS QTE_VENDUE,
|
||||
SUM(m.MntMvtHt) AS CA_HT,
|
||||
SUM(m.MntMvtTTC) AS CA_TTC,
|
||||
SUM(m.MargeMvt) AS MARGE
|
||||
FROM MvtArt m
|
||||
JOIN ART_GAMME_SAISON ags ON ags.ARTNOID = m.ArtNoId
|
||||
JOIN GAMMES g ON g.NO_ID = ags.IDGAMME
|
||||
WHERE m.GenreMvt = 3
|
||||
AND m.DatMvt BETWEEN $1 AND $2
|
||||
AND m.Site LIKE $3
|
||||
GROUP BY g.CODE, g.LIBELLE
|
||||
ORDER BY CA_TTC DESC
|
||||
`, [dateDebut, dateFin, `%${site}%`]);
|
||||
|
||||
res.json({ dateDebut, dateFin, ca_par_gamme: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,136 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getPool } = require('../config/database');
|
||||
|
||||
// GET /api/schema - Structure complète de la base de données
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
table_schema AS TABLE_SCHEMA,
|
||||
table_name AS TABLE_NAME,
|
||||
table_type AS TABLE_TYPE,
|
||||
column_name AS COLUMN_NAME,
|
||||
ordinal_position AS ORDINAL_POSITION,
|
||||
data_type AS DATA_TYPE,
|
||||
character_maximum_length AS CHARACTER_MAXIMUM_LENGTH,
|
||||
numeric_precision AS NUMERIC_PRECISION,
|
||||
numeric_scale AS NUMERIC_SCALE,
|
||||
is_nullable AS IS_NULLABLE,
|
||||
column_default AS COLUMN_DEFAULT
|
||||
FROM information_schema.tables
|
||||
JOIN information_schema.columns USING (table_schema, table_name)
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name, ordinal_position
|
||||
`);
|
||||
|
||||
const schema = {};
|
||||
for (const row of result.rows) {
|
||||
const key = `${row.table_schema}.${row.table_name}`;
|
||||
if (!schema[key]) {
|
||||
schema[key] = {
|
||||
schema: row.table_schema,
|
||||
table: row.table_name,
|
||||
type: row.table_type,
|
||||
columns: [],
|
||||
};
|
||||
}
|
||||
schema[key].columns.push({
|
||||
name: row.column_name,
|
||||
position: row.ordinal_position,
|
||||
type: row.data_type,
|
||||
maxLength: row.character_maximum_length,
|
||||
precision: row.numeric_precision,
|
||||
scale: row.numeric_scale,
|
||||
nullable: row.is_nullable === 'YES',
|
||||
default: row.column_default,
|
||||
});
|
||||
}
|
||||
|
||||
const tables = Object.values(schema);
|
||||
res.json({
|
||||
database: process.env.PG_DB,
|
||||
server: process.env.PG_HOST,
|
||||
tableCount: tables.filter(t => t.type === 'BASE TABLE').length,
|
||||
viewCount: tables.filter(t => t.type === 'VIEW').length,
|
||||
tables,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Erreur connexion base de données', detail: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/schema/tables?search= - Liste des tables avec nombre de lignes (estimé)
|
||||
router.get('/tables', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const search = req.query.search || '';
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
schemaname AS TABLE_SCHEMA,
|
||||
tablename AS TABLE_NAME,
|
||||
'BASE TABLE' AS TABLE_TYPE,
|
||||
n_live_tup AS ROW_COUNT
|
||||
FROM pg_stat_user_tables
|
||||
WHERE tablename LIKE $1
|
||||
ORDER BY n_live_tup DESC, tablename
|
||||
`, [`%${search}%`]);
|
||||
|
||||
res.json({ count: result.rows.length, tables: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Erreur connexion base de données', detail: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/schema/top?limit= - Top tables par nombre de lignes
|
||||
router.get('/top', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const limitNum = Math.max(1, Math.min(parseInt(req.query.limit) || 50, 200));
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT tablename AS TABLE_NAME, 'BASE TABLE' AS TABLE_TYPE, n_live_tup AS ROW_COUNT
|
||||
FROM pg_stat_user_tables
|
||||
ORDER BY n_live_tup DESC
|
||||
LIMIT $1
|
||||
`, [limitNum]);
|
||||
|
||||
res.json({ tables: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Erreur connexion base de données', detail: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/schema/tables/:tableName - Colonnes d'une table
|
||||
router.get('/tables/:tableName', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
column_name AS COLUMN_NAME,
|
||||
ordinal_position AS ORDINAL_POSITION,
|
||||
data_type AS DATA_TYPE,
|
||||
character_maximum_length AS CHARACTER_MAXIMUM_LENGTH,
|
||||
numeric_precision AS NUMERIC_PRECISION,
|
||||
numeric_scale AS NUMERIC_SCALE,
|
||||
is_nullable AS IS_NULLABLE,
|
||||
column_default AS COLUMN_DEFAULT
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = $1 AND table_schema = 'public'
|
||||
ORDER BY ordinal_position
|
||||
`, [req.params.tableName]);
|
||||
|
||||
if (!result.rows.length)
|
||||
return res.status(404).json({ error: `Table '${req.params.tableName}' introuvable` });
|
||||
|
||||
res.json({ table: req.params.tableName, columns: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Erreur connexion base de données', detail: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,111 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getPool } = require('../config/database');
|
||||
|
||||
// GET /api/stock?site=&page=&limit= - Stock global
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { site = '', page = 1, limit = 100 } = req.query;
|
||||
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||
const offsetNum = (pageNum - 1) * limitNum;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
a.CODEIN, a.LIBELLE1,
|
||||
s.Site, s.QTE, s.Prmp, s.ValStock, s.PV,
|
||||
s.StockDispo, s.StockMort, s.StockColis,
|
||||
s.DerniereVente, s.DerniereReception, s.PremiereVente,
|
||||
s.NbJoursDernierMouvement, s.NbJoursDerniereVente,
|
||||
s.NbJoursDerniereReception, s.InterditAchat
|
||||
FROM Cube_Stock s
|
||||
JOIN ARTICLES a ON a.NO_ID = s.ArtNoId
|
||||
WHERE s.Site LIKE $1
|
||||
ORDER BY a.LIBELLE1, s.Site
|
||||
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||
`, [`%${site}%`]);
|
||||
|
||||
res.json({ page: pageNum, limit: limitNum, stock: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/stock/article/:id - Stock d'un article par site
|
||||
router.get('/article/:id', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
s.Site, s.QTE, s.Prmp, s.ValStock, s.PV,
|
||||
s.StockDispo, s.StockMort, s.StockColis,
|
||||
s.DerniereVente, s.DerniereReception, s.PremiereVente,
|
||||
s.NbJoursDernierMouvement, s.NbJoursDerniereVente,
|
||||
s.NbJoursDerniereReception, s.InterditAchat, s.CODEFOU
|
||||
FROM Cube_Stock s
|
||||
WHERE s.ArtNoId = $1
|
||||
ORDER BY s.Site
|
||||
`, [req.params.id]);
|
||||
|
||||
res.json({ artNoId: req.params.id, stock: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/stock/site/:site - Tous les articles d'un site
|
||||
router.get('/site/:site', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const { page = 1, limit = 100 } = req.query;
|
||||
const pageNum = Math.max(1, parseInt(page) || 1);
|
||||
const limitNum = Math.max(1, Math.min(parseInt(limit) || 100, 1000));
|
||||
const offsetNum = (pageNum - 1) * limitNum;
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
a.NO_ID AS ART_NO_ID, a.CODEIN, a.LIBELLE1,
|
||||
s.QTE, s.Prmp, s.ValStock, s.PV,
|
||||
s.StockDispo, s.StockMort, s.DerniereVente, s.DerniereReception,
|
||||
s.NbJoursDerniereVente, s.NbJoursDerniereReception
|
||||
FROM Cube_Stock s
|
||||
JOIN ARTICLES a ON a.NO_ID = s.ArtNoId
|
||||
WHERE s.Site = $1
|
||||
ORDER BY s.QTE DESC
|
||||
LIMIT ${limitNum} OFFSET ${offsetNum}
|
||||
`, [req.params.site]);
|
||||
|
||||
res.json({ site: req.params.site, page: pageNum, limit: limitNum, stock: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/stock/valorisation - Valorisation du stock par site
|
||||
router.get('/valorisation', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
Site,
|
||||
COUNT(*) AS NB_ARTICLES,
|
||||
SUM(QTE) AS QTE_TOTALE,
|
||||
SUM(ValStock) AS VAL_STOCK,
|
||||
SUM(QTE * PV) AS VAL_PV_THEORIQUE,
|
||||
AVG(Prmp) AS PRMP_MOYEN
|
||||
FROM Cube_Stock
|
||||
WHERE QTE > 0
|
||||
GROUP BY Site
|
||||
ORDER BY VAL_STOCK DESC
|
||||
`);
|
||||
|
||||
res.json({ valorisation: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,19 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getPool } = require('../config/database');
|
||||
|
||||
// GET /api/sync/status - Dernier état de chaque table synchronisée
|
||||
router.get('/status', async (req, res) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
const result = await pool.query(
|
||||
`SELECT table_name, last_sync, rows_synced, status, error_msg
|
||||
FROM sync_log ORDER BY last_sync DESC NULLS LAST`
|
||||
);
|
||||
res.json({ sync: result.rows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in new issue
Block a user