From 0186a7318e8587d536ad97e3852244dfc5fa459c Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 21 May 2026 20:27:22 -0400 Subject: [PATCH] =?UTF-8?q?fix(ops/campaigns):=20correct=20row=20counts=20?= =?UTF-8?q?in=20Step=201=20=E2=80=94=20Link=20Order=20CSV=20had=20no=20hea?= =?UTF-8?q?der?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Step 1 file-upload widgets displayed `(newlines) - 1` for both CSVs, assuming both files have a header row to discount. This breaks for the Giftbit Link Order export which is headerless (one URL per line): a 3-URL file was showing "2 cartes-cadeaux" because the parser ate URL #1 as a fake header. The backend parser was already correct (detects Link Order vs Campaign format by inspecting the first line). The bug was UI-only — the count display reused the same arithmetic for both formats. Fix: introduce countMapRows / countGiftRows helpers that mirror the backend's format detection. Map CSV subtracts 2 (preamble + header). Gift CSV subtracts 0 for Link Order (headerless) or 1 for Campaign export (with header). Plus a "(format: Link Order)" hint next to the count so the user sees which detection path was taken. Co-Authored-By: Claude Opus 4.7 --- .../campaigns/pages/CampaignNewPage.vue | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/apps/ops/src/modules/campaigns/pages/CampaignNewPage.vue b/apps/ops/src/modules/campaigns/pages/CampaignNewPage.vue index 741f0b2..22e241d 100644 --- a/apps/ops/src/modules/campaigns/pages/CampaignNewPage.vue +++ b/apps/ops/src/modules/campaigns/pages/CampaignNewPage.vue @@ -22,7 +22,7 @@
- ✓ {{ Math.max(0, (mapPreview.match(/\n/g)||[]).length - 1) }} lignes lues + ✓ {{ countMapRows(mapPreview) }} lignes de contacts (préambule + header retirés)
@@ -40,7 +40,8 @@
- ✓ {{ Math.max(0, (giftPreview.match(/\n/g)||[]).length - 1) }} cartes-cadeaux + ✓ {{ countGiftRows(giftPreview) }} cartes-cadeaux + (format: {{ giftFormatHint }})
@@ -234,6 +235,44 @@ function readFile (file) { async function readMapFile (file) { if (file) mapPreview.value = await readFile(file) } async function readGiftFile (file) { if (file) giftPreview.value = await readFile(file) } +// Counts must mirror the backend parser exactly so the user sees the same +// numbers in the preview as what Step 2 will receive. + +// Map CSV format: 1-line title preamble + header row + N data rows. +// Returns N (the # of contact lines, excluding the preamble and header). +function countMapRows (text) { + if (!text) return 0 + const lines = text.split(/\r?\n/).filter(l => l.trim()) + // -1 for preamble, -1 for header + return Math.max(0, lines.length - 2) +} + +// Giftbit CSV: TWO formats +// 1. "Link Order" — headerless, one URL per line (each URL = 1 gift) +// 2. "Campaign export" — header row + N data rows (-1 for header) +// Detect like the backend: first non-empty line is a bare URL with no +// separator → no header. +function countGiftRows (text) { + if (!text) return 0 + const cleaned = text.replace(/^/, '').trim() + if (!cleaned) return 0 + const firstLine = cleaned.split(/\r?\n/, 1)[0].trim() + const isLinkOrder = /^https?:\/\/\S+$/.test(firstLine) && + !firstLine.includes(',') && !firstLine.includes('\t') && !firstLine.includes('|') + const lines = cleaned.split(/\r?\n/).filter(l => l.trim()) + return isLinkOrder ? lines.length : Math.max(0, lines.length - 1) +} + +// Show "Link Order" or "Campaign export" hint next to the gift count +const giftFormatHint = computed(() => { + if (!giftPreview.value) return '' + const firstLine = giftPreview.value.replace(/^/, '').trim().split(/\r?\n/, 1)[0].trim() + if (/^https?:\/\/\S+$/.test(firstLine) && !firstLine.includes(',') && !firstLine.includes('\t') && !firstLine.includes('|')) { + return 'Link Order' + } + return 'Campaign export' +}) + async function goPreview () { if (!mapPreview.value || !giftPreview.value) return parsing.value = true