fix(campaigns/parse): keep no-name rows + surface skip breakdown
Map CSV rows that had a valid email but no name in the source column were silently dropped at parsing — that's why a campaign would end up with N unpaired Giftbit shortlinks for N "missing" contacts that weren't actually missing, just nameless. The send worker already handles a missing firstname by substituting "cher client" / "dear customer", so dropping the row was wasteful. Now we keep the contact and surface a name_warning on the row so the operator can either edit the firstname in Step 2 or accept the default. Also added counters for previously-silent skip paths: - duplicate: row's email was already seen above (1 gift / household consolidation, depending on the multi setting) - multi_skip: couple skipped because multi='skip' was selected Wizard Step 2 imbalance banner now ventilates the skip breakdown so the operator understands exactly where the N "missing" contacts went: Ventilation des contacts droppés au parsing du Map CSV (sur 213 lignes brutes) : 8 sans email valide · 5 emails en double · 0 couples ignorés · 3 sans nom (gardés, utilisent "cher client" à l'envoi) Unrelated reassurance on the question that triggered this: language fallback to French is already in place (matchCustomer returns language:'fr' on miss, worker reads (r.language || 'fr')) so any unmatched recipient gets the FR template, never an English one by default. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
f6d06d9b34
commit
5c55087198
|
|
@ -220,6 +220,18 @@
|
|||
il y a plus de cartes-cadeaux que de contacts. Le surplus sera perdu si
|
||||
la campagne est envoyée tel quel.
|
||||
</div>
|
||||
<!-- Parser skip breakdown: tells the operator exactly WHERE the missing
|
||||
contacts went (no_email = lost forever; duplicate = couple/foyer
|
||||
de-duped; multi_skip = couple skipped by their multi setting). -->
|
||||
<div v-if="parseSkipped && (parseSkipped.no_email || parseSkipped.duplicate || parseSkipped.multi_skip)"
|
||||
class="q-mt-sm text-caption" style="color:#7C3F00">
|
||||
<q-icon name="info" size="14px" /> Ventilation des contacts droppés au parsing du Map CSV
|
||||
(sur {{ parseSkipped.total_rows }} lignes brutes) :
|
||||
<span v-if="parseSkipped.no_email"> <strong>{{ parseSkipped.no_email }}</strong> sans email valide ·</span>
|
||||
<span v-if="parseSkipped.duplicate"> <strong>{{ parseSkipped.duplicate }}</strong> emails en double (déjà vu plus haut) ·</span>
|
||||
<span v-if="parseSkipped.multi_skip"> <strong>{{ parseSkipped.multi_skip }}</strong> couples ignorés (selon le réglage "Emails multiples") ·</span>
|
||||
<span v-if="parseSkipped.no_name"> <strong>{{ parseSkipped.no_name }}</strong> sans nom (gardés, utilisent "cher client" à l'envoi)</span>
|
||||
</div>
|
||||
</q-banner>
|
||||
|
||||
<!-- ✓ Paired recipients (will be sent) ────────────────────────── -->
|
||||
|
|
@ -621,6 +633,10 @@ const sending = ref(false)
|
|||
const recipients = ref([])
|
||||
const unpairedContacts = ref([])
|
||||
const unusedGifts = ref([])
|
||||
// Map CSV parse skip counters surfaced from the hub so the operator can see
|
||||
// exactly which rows didn't make it into the pairing (no email, duplicates,
|
||||
// multi-skip).
|
||||
const parseSkipped = ref(null)
|
||||
|
||||
// row_index (#1, #2, ...) is the source-CSV position — invaluable for the
|
||||
// user to cross-reference what they see here against the file they uploaded.
|
||||
|
|
@ -820,6 +836,7 @@ async function goPreview () {
|
|||
recipients.value = r.recipients || []
|
||||
unpairedContacts.value = r.unpaired_contacts || []
|
||||
unusedGifts.value = r.unused_gifts || []
|
||||
parseSkipped.value = r.skipped || null
|
||||
step.value = 2
|
||||
} catch (e) {
|
||||
$q.notify({ type: 'negative', message: 'Erreur de parsing: ' + e.message })
|
||||
|
|
|
|||
|
|
@ -390,7 +390,7 @@ function parseMapCsv (text, multi = 'first') {
|
|||
const rows = parseCsv(text, { skipPreamble: true })
|
||||
const contacts = []
|
||||
const seen = new Set()
|
||||
let skippedNoEmail = 0, skippedNoName = 0
|
||||
let skippedNoEmail = 0, skippedNoName = 0, skippedDuplicate = 0, skippedMultiSkip = 0
|
||||
|
||||
for (const r of rows) {
|
||||
// Pull emails from either source column
|
||||
|
|
@ -404,10 +404,15 @@ function parseMapCsv (text, multi = 'first') {
|
|||
const sendEmails = multi === 'split' ? emails
|
||||
: multi === 'skip' ? (emails.length > 1 ? [] : emails)
|
||||
: emails.slice(0, 1)
|
||||
if (!sendEmails.length) continue
|
||||
if (!sendEmails.length) { skippedMultiSkip++; continue }
|
||||
|
||||
// We used to skip rows without a name here. That dropped the contact AND
|
||||
// wasted its paired Giftbit shortlink — the worker already defaults
|
||||
// firstname to "cher client" / "dear customer" when missing, so we now
|
||||
// keep the row and just flag it with a name warning.
|
||||
const full = (r['nom au compte'] || r["nom à l'adresse"] || '').trim()
|
||||
if (!full) { skippedNoName++; continue }
|
||||
const hasName = !!full
|
||||
if (!hasName) skippedNoName++ // informational counter, NOT a continue
|
||||
const parts = full.split(/\s+/, 2)
|
||||
const firstnameRaw = parts[0] || ''
|
||||
const lastnameRaw = parts.length > 1 ? full.slice(parts[0].length).trim() : ''
|
||||
|
|
@ -419,7 +424,7 @@ function parseMapCsv (text, multi = 'first') {
|
|||
const firstname = cleanName(firstnameRaw)
|
||||
const lastname = cleanName(lastnameRaw)
|
||||
const name_warnings = {
|
||||
firstname: nameWarning(firstname),
|
||||
firstname: !hasName ? 'pas de nom dans le CSV — utilisera "cher client" à l\'envoi' : nameWarning(firstname),
|
||||
lastname: nameWarning(lastname),
|
||||
}
|
||||
const cleaned_changed = (firstname !== firstnameRaw || lastname !== lastnameRaw)
|
||||
|
|
@ -429,7 +434,7 @@ function parseMapCsv (text, multi = 'first') {
|
|||
const phone = normalizePhone(r['telephone au compte'] || r["telephone à l'adresse"] || '')
|
||||
|
||||
for (const em of sendEmails) {
|
||||
if (seen.has(em)) continue
|
||||
if (seen.has(em)) { skippedDuplicate++; continue }
|
||||
seen.add(em)
|
||||
contacts.push({
|
||||
firstname, lastname,
|
||||
|
|
@ -443,7 +448,16 @@ function parseMapCsv (text, multi = 'first') {
|
|||
})
|
||||
}
|
||||
}
|
||||
return { contacts, skipped: { no_email: skippedNoEmail, no_name: skippedNoName, total_rows: rows.length } }
|
||||
return {
|
||||
contacts,
|
||||
skipped: {
|
||||
no_email: skippedNoEmail,
|
||||
no_name: skippedNoName, // informational only — these rows are KEPT now
|
||||
duplicate: skippedDuplicate,
|
||||
multi_skip: skippedMultiSkip,
|
||||
total_rows: rows.length,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── Giftbit CSV parsing ──────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user