feat(articles): expose and filter code centrale (artcentrale)

The code centrale is LaFoir'Fouille's 11-digit product identifier
(prefix 1000x). Only products referenced by the central purchasing
office have one: ~355 800 of 427 700 articles.

The column was already synced from SQL Server and returned by the
detail route via `a.*`, but was absent from the list route and could
not be filtered on.

- list route: select artcentrale, add `artcentrale` (partial match)
  and `has_artcentrale=1|0` query params
- filter matches on prefix AND length 11, so malformed values with a
  valid prefix ('10000', '10000192289P') are excluded
- COALESCE in the predicate: without it, NOT (NULL LIKE '1000%') is
  NULL and the 61 442 NULL rows would vanish from has_artcentrale=0
- normalise '' to null in both routes so they agree
- sync: use safeStr so empty values are stored as NULL, not ''

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 4.8 committed 2026-07-09 09:09:25 +02:00
1 parent 62249e6c13
commit f00b9c4b14
3 files changed
+30 -5

No files matched your search

+19 -3
View File
@@ -2,11 +2,12 @@ const express = require('express');
const router = express.Router();
const { getPool } = require('../config/database');
// GET /api/articles?search=&codein=&ean=&actif=&codefou=&page=&limit=
// GET /api/articles?search=&codein=&ean=&actif=&codefou=&artcentrale=&has_artcentrale=&page=&limit=
router.get('/', async (req, res) => {
try {
const pool = getPool();
const { search = '', codein = '', ean = '', actif = '', codefou = '', page = 1, limit = 50 } = req.query;
const { search = '', codein = '', ean = '', actif = '', codefou = '',
artcentrale = '', has_artcentrale = '', 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;
@@ -16,6 +17,7 @@ router.get('/', async (req, res) => {
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,
NULLIF(TRIM(a.ARTCENTRALE), '') AS ARTCENTRALE,
ai.PRIX_VENTE_MINI, ai.PRIX_VENTE_MAXI, ai.ECO_TTC,
ai.ON_WEB, ai.INTERDIT_REMISE, ai.NOMPHOTO,
ai.DATEDEBVENTE, ai.DATEFINVENTE,
@@ -50,9 +52,22 @@ router.get('/', async (req, res) => {
AND ($4 = '%%' OR EXISTS (
SELECT 1 FROM ARTFOU1 f WHERE f.ART_NO_ID = a.NO_ID AND f.CODE LIKE $4
))
AND ($6 = '%%' OR TRIM(a.ARTCENTRALE) LIKE $6)
-- code centrale LaFoir'Fouille : préfixe 1000x sur 11 chiffres.
-- COALESCE car les articles non référencés valent '' ou NULL.
AND ($7 = '' OR (
$7 = '1' AND TRIM(COALESCE(a.ARTCENTRALE, '')) LIKE '1000%'
AND LENGTH(TRIM(COALESCE(a.ARTCENTRALE, ''))) = 11
) OR (
$7 = '0' AND NOT (
TRIM(COALESCE(a.ARTCENTRALE, '')) LIKE '1000%'
AND LENGTH(TRIM(COALESCE(a.ARTCENTRALE, ''))) = 11
)
))
ORDER BY a.LIBELLE1
LIMIT ${limitNum} OFFSET ${offsetNum}
`, [`%${search}%`, `%${codein}%`, `%${ean}%`, `%${codefou}%`, actif]);
`, [`%${search}%`, `%${codein}%`, `%${ean}%`, `%${codefou}%`, actif,
`%${artcentrale}%`, has_artcentrale]);
const articles = result.rows.map(a => {
const photoCode = a.nomphoto ? a.nomphoto.replace(/\.[^.]+$/, '') : null;
@@ -123,6 +138,7 @@ router.get('/:id', async (req, res) => {
res.json({
article: {
...art,
artcentrale: art.artcentrale?.trim() || null,
photo_url: photoCode ? `/api/articles/${req.params.id}/photo` : null,
photo_url_large: photoCode ? `/api/articles/${req.params.id}/photo?size=large` : null,
},