feat(campaigns): one-click Giftbit admin lookup per recipient
Manual workaround for redemption status until /gifts/{uuid} polling
ships (task #25). The trailing path segment of the Giftbit shortlink
is the lookup key for Giftbit's admin search:
http://gft.link/4kpZMApLK4B
→ https://app.giftbit.com/app/rewards?search=4kpZMApLK4B
Surfaced in three places:
- Inventory page row: 🔗 button next to the copy-URL action
- Campaign detail page recipient table: same button next to the
Giftbit shortlink
- CSV report: new giftbit_admin_url column for bulk audits in Excel
(one click per row, no manual concat)
Defensive: only renders if the trailing segment is ≥4 chars (avoids
producing useless searches on malformed/test URLs).
🤖 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
feeae6eb40
commit
85ad66f103
|
|
@ -96,6 +96,11 @@
|
|||
<a :href="props.row.gift_url" target="_blank" class="text-grey-7" style="font-family:monospace; font-size:0.78rem">
|
||||
{{ shortLink(props.row.gift_url) }}
|
||||
</a>
|
||||
<q-btn v-if="giftbitAdminUrl(props.row.gift_url)"
|
||||
flat dense round size="xs" icon="open_in_new" color="primary"
|
||||
:href="giftbitAdminUrl(props.row.gift_url)" target="_blank" class="q-ml-xs">
|
||||
<q-tooltip>Voir sur Giftbit admin (vérifier statut de redemption)</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
</template>
|
||||
<template v-slot:body-cell-error="props">
|
||||
|
|
@ -155,6 +160,16 @@ function statusLabel (s) {
|
|||
}
|
||||
function shortLink (u) { return (u || '').replace(/^https?:\/\//, '').slice(0, 28) + ((u || '').length > 35 ? '…' : '') }
|
||||
|
||||
// Deep link into Giftbit's admin so the operator can verify redemption
|
||||
// status manually until the /gifts/{uuid} API polling (task #25) lands.
|
||||
// The trailing segment of the shortlink URL becomes the search query.
|
||||
function giftbitAdminUrl (giftUrl) {
|
||||
if (!giftUrl) return null
|
||||
const code = giftUrl.replace(/\/+$/, '').split('/').pop()
|
||||
if (!code || code.length < 4) return null
|
||||
return `https://app.giftbit.com/app/rewards?search=${encodeURIComponent(code)}`
|
||||
}
|
||||
|
||||
const reportCsvUrl = computed(() => campaignReportCsvUrl(id))
|
||||
|
||||
const sentRatio = computed(() => {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,11 @@
|
|||
<q-btn flat dense round size="xs" icon="content_copy" @click="copy(props.row.gift_url)" class="q-ml-xs">
|
||||
<q-tooltip>Copier l'URL Giftbit (à coller dans une nouvelle campagne pour réassigner)</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn v-if="giftbitAdminUrl(props.row.gift_url)"
|
||||
flat dense round size="xs" icon="open_in_new" color="primary"
|
||||
:href="giftbitAdminUrl(props.row.gift_url)" target="_blank" class="q-ml-xs">
|
||||
<q-tooltip>Voir l'historique sur Giftbit (statut de redemption)</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
</template>
|
||||
<template v-slot:body-cell-expires="props">
|
||||
|
|
@ -241,6 +246,21 @@ function shorten (u) {
|
|||
if (!u) return ''
|
||||
return u.replace(/^https?:\/\//, '').slice(0, 36) + (u.length > 40 ? '…' : '')
|
||||
}
|
||||
|
||||
// Build a deep link into Giftbit's admin panel for a given shortlink so
|
||||
// operators can confirm redemption status manually until task #25 wires
|
||||
// the /gifts/{uuid} API polling. The trailing path segment of the
|
||||
// shortlink URL is what Giftbit's reward search uses as the lookup key.
|
||||
// "http://gft.link/4kpZMApLK4B"
|
||||
// → "https://app.giftbit.com/app/rewards?search=4kpZMApLK4B"
|
||||
function giftbitAdminUrl (giftUrl) {
|
||||
if (!giftUrl) return null
|
||||
// Last non-empty path segment (strip trailing slashes if any)
|
||||
const segments = giftUrl.replace(/\/+$/, '').split('/')
|
||||
const code = segments[segments.length - 1]
|
||||
if (!code || code.length < 4) return null
|
||||
return `https://app.giftbit.com/app/rewards?search=${encodeURIComponent(code)}`
|
||||
}
|
||||
function formatDate (iso) {
|
||||
if (!iso) return ''
|
||||
return new Date(iso).toLocaleString('fr-CA', { dateStyle: 'medium', timeStyle: 'short' })
|
||||
|
|
|
|||
|
|
@ -1838,7 +1838,8 @@ async function handle (req, res, method, path) {
|
|||
if (!c) return json(res, 404, { error: 'not found' })
|
||||
const headers = [
|
||||
'row', 'firstname', 'lastname', 'email', 'phone', 'language', 'customer_id',
|
||||
'civic_address', 'city', 'postal_code', 'gift_value_cents', 'gift_url', 'giftbit_uuid',
|
||||
'civic_address', 'city', 'postal_code', 'gift_value_cents',
|
||||
'gift_url', 'giftbit_uuid', 'giftbit_admin_url',
|
||||
'gift_token', 'gift_expires_at', 'gift_revoked', 'gift_redirected_count', 'gift_first_redirected_at',
|
||||
'status', 'excluded', 'sent_at', 'opened_at', 'clicked_at',
|
||||
'gift_link_clicked', 'gift_clicked_at',
|
||||
|
|
@ -1851,9 +1852,19 @@ async function handle (req, res, method, path) {
|
|||
}
|
||||
const lines = [headers.join(',')]
|
||||
for (const r of (c.recipients || [])) {
|
||||
// Build Giftbit admin search URL for manual redemption check
|
||||
// (until /gifts/{uuid} API polling lands — see task #25).
|
||||
let giftbitAdminUrl = ''
|
||||
if (r.gift_url) {
|
||||
const code = String(r.gift_url).replace(/\/+$/, '').split('/').pop()
|
||||
if (code && code.length >= 4) {
|
||||
giftbitAdminUrl = `https://app.giftbit.com/app/rewards?search=${encodeURIComponent(code)}`
|
||||
}
|
||||
}
|
||||
lines.push([
|
||||
r.row_index, r.firstname, r.lastname, r.email, r.phone, r.language, r.customer_id,
|
||||
r.civic_address, r.city, r.postal_code, r.gift_value_cents, r.gift_url, r.giftbit_uuid,
|
||||
r.civic_address, r.city, r.postal_code, r.gift_value_cents,
|
||||
r.gift_url, r.giftbit_uuid, giftbitAdminUrl,
|
||||
r.gift_token, r.gift_expires_at, r.gift_revoked ? 'true' : 'false',
|
||||
r.gift_redirected_count || 0, r.gift_first_redirected_at,
|
||||
r.status, r.excluded ? 'true' : 'false', r.sent_at, r.opened_at, r.clicked_at,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user