- Replace month dropdown with date picker (Du/Au) + Appliquer button
- Add fournisseur filter dropdown (client-side, all suppliers from current data)
- Totals update live when fournisseur filter changes
- SQL: use BETWEEN date range instead of TO_CHAR month match
- Create user button: proper 3-col form layout so button stays on one line
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Non-admin users see gamme as read-only badge (no select)
- Export button hidden for non-admin in grid
- Save drafts button hidden for non-admin
- Exports page hidden in sidebar for non-admin
- Create user button: explicit blue style (was unreadable with apple-btn-primary)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace last_reception CTE with direct LEFT JOIN artfou1 WHERE preference=true (fournisseur
principal de l'article). Use ABS(SUM(mntmvtttc)) instead of SUM(-mntmvtttc). Both fournisseur
and nomenclature queries now use LEFT JOIN to include all sold articles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fouident is the canonical supplier identity table with correct names:
- D005 = 'DEPOT' (not 'BAZAR 5000' which was a historical fouadr1 alias)
- B0071 = 'BAZAR 5000' (suspended, actif=false)
- M0341 = 'MOREL' (not 'MOREL LORY')
Updated: pgGetFournisseurs, pgGetArticlesByFournisseur, pgGetCaByFournisseur
Removed the D005 CASE WHEN hardcode override (no longer needed).
Also remove fouadr1 JOIN from analytics query (replaced by fouident).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
D005 is the main active supplier (95k+ articles, fournisseur principal)
but its historical name in the FF Nancy ERP is 'BAZAR 5000' since 2004.
The store no longer associates this code with BAZAR 5000 — it is their
Dépôt. Override via CASE WHEN in SQL since the ERP name cannot be changed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous approach (artfou1 DISTINCT ON no_id DESC) picked the most recent
artfou1 record, but artfou1 is a commercial reference table that may not
reflect actual current sourcing.
New approach: for each article, find the most recent reception movement
(genremvt IN (1,2)) in mvtart — this is the actual last delivery. Join
artfou1 to get the supplier code (ORDER BY datmvt DESC, af.no_id DESC to
break ties). This correctly attributes CA to whoever last physically
delivered the goods, not to a historical commercial reference.
Articles with no reception history are excluded (no INNER JOIN fallback
needed as all sold articles must have been received at least once).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DISTINCT ON with ORDER BY no_id ASC was picking the oldest supplier entry
(BAZAR 5000 historically), attributing CA to a supplier the store no longer
uses. Changing to DESC picks the most recently inserted artfou1 row, which
represents the current active supplier for each article.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
artfou1 has multiple rows per article (multi-supplier), causing each mvtart
movement to be counted N times in the JOIN. Use a CTE with DISTINCT ON
(art_no_id) ORDER BY no_id to pick one canonical supplier per article,
matching the same pattern used in pgGetArticlesByFournisseur.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- page.tsx: move formatCA into client component (functions not serializable
across server→client boundary in Next.js — root cause of 500)
- client.tsx: remove unused BarChart2 import
- client.tsx: replace useSearchParams() with direct URL construction to
avoid Suspense boundary requirement
- pg-ff-client: use OR conditions instead of IN() for Drizzle compatibility
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pg-ff-client: replace ANY(ARRAY[...]) with IN(...) for Drizzle compatibility
- sidebar: change Analytics icon from BarChart2 (duplicate) to TrendingUp
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
EOF
- pg-ff-client: add pgGetCaByFournisseur() and pgGetCaByNomenclature()
returning CA TTC per site and month for monthly N vs N-1 comparison
- analytics/page.tsx: server component with pivot logic to reshape raw
query data into store-separated columns (292/579/total) with evolution %
- analytics/client.tsx: client controls (mode dropdown, month selector)
and responsive table with color-coded evolution badges
- sidebar: add "Analytics" link to navigation
Features:
- Switch between "Fournisseur" and "Nomenclature" views
- Select any month for comparison vs same month prior year
- Columns split by store: Frouard/Nancy (292), Houdemont (579), Total
- Each store shows: CA this month, CA N-1, evolution %
- Footer totals row matching sum of detail rows
- FR locale currency formatting (€)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- score-engine: add hard guard (wQty=0 && wCa=0 → score=0) before any
calculation to prevent edge cases (residual margin, percentile rank)
from producing non-zero scores on unsold articles
- score-engine: set inactivity=24 when product has never sold, so the
regularityMultiplier penalty (0.3) is correctly applied instead of 1
- pg-ff-client: remove erroneous negation on qte_recue — entry movements
have positive qtemvt in the DB, negating them produced negative values
that failed the > 0 check, hiding all entrées in tooltips and modals
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The ranking bulk endpoint is capped at 500 by the API server, making
it useless for full supplier matching. Per-article ranking (codein filter)
works correctly and is now called only for articles WITH sales data.
getProductRows restructured:
- Phase 2A: mensuel + commandes in parallel (all articles)
- Phase 2B: referentiel + ranking in parallel, only for articlesWithData
(articles that appear in mensuelMap)
For a 45060-article supplier with 5446 with sales:
- Referentiel: 45060 → 5446 calls (88% reduction)
- Ranking: 45060 → 5446 calls (88% reduction)
totalRankedProducts fetched from GET /api/ranking?limit=500 count.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The bulk ranking response (GET /api/ranking?limit=30000) may use
'gencod' instead of 'codein' as the article identifier. Added:
- gtin→codein lookup map built from article gtin fields
- Match entries by entry.codein first, then by entry.gencod→article.gtin
- Diagnostic logging of the first entry's keys and values to identify
the correct identifier field in the API response
- Increased fallback per-article batchSize from 20 to 50
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove actif/suspendu filter that caused wrong stats (893€ CA, 249 units)
by incorrectly excluding products with sales. The parallel pagination
and bulk ranking fixes are sufficient to handle large suppliers.
- Increase getMensuelByArticles and getReferentielByArticles batchSize
from 20 to 50, reducing sequential batch iterations by ~2.5x.
- Filter gamme Y products with zero 12-month sales from returned rows.
Reduces grid size (e.g. S053: 14775→fewer rows) without affecting
products that actually sold. Y products with sales are preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Article pagination:
- fetchAllPages fallback now fetches 10 pages in parallel instead of
sequentially. Stops as soon as any page returns < pageSize items.
For S053 (30 pages), reduces sequential time by ~10x.
Ranking:
- getRankingByArticles now fetches the entire network ranking in one
call (GET /api/ranking?limit=30000) and matches by codein.
Replaces foucentrale bulk (which returned 0) and per-article fallback
(which would be 8889 calls for S053).
- totalRankedProducts = actual number of entries returned by the API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Performance fixes for large suppliers (14k+ articles):
- fetchAllPages now fetches page 1, reads total count from response,
then fetches remaining pages in parallel (10 at a time) instead of
sequentially. Falls back to sequential if no count in response.
- getProductRows filters out suspended/inactive articles before calling
getMensuelByArticles and getReferentielByArticles, reducing API calls
from N_total to N_active (e.g. 14775→~2500 for S053).
- getRankingByArticles receives only active articles for bulk fetch.
- FfArticle now exposes actif and suspendu fields.
Gamme Y support:
- GammeSelect: add Y option with violet color
- GridFilterBar: add Y filter button with violet color
- BulkActionToolbar: add Y bulk assignment button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getRankingByArticles now uses GET /api/ranking?foucentrale=<code>
to fetch all rankings for a supplier in a single request instead of
making one request per article. For a 500-article supplier this goes
from ~500 HTTP calls to 2 (count + bulk), eliminating the timeout
that caused the client-side crash on large suppliers.
Falls back to per-article requests if no codeFournisseur is provided
or if the bulk endpoint fails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
handleSnapshot was only looking at active draftChanges to build the
snapshot. After validation (Valider), drafts are cleared but changes
are applied to row.codeGamme via applyDraftsToRows. This caused
handleSnapshot (Camera/Export) to save an empty snapshot that
overwrote the real one, losing all gamme changes on page reload.
Now handleSnapshot compares (draftChanges[codein] ?? row.codeGamme)
vs row.codeGammeInit to detect ALL changes, whether drafted or
already validated. Uses codeGammeInit as "before" reference (API
value, never modified).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>