diff --git a/apps/ops/src/modules/campaigns/pages/CampaignDetailPage.vue b/apps/ops/src/modules/campaigns/pages/CampaignDetailPage.vue
index c49ee03..4c25453 100644
--- a/apps/ops/src/modules/campaigns/pages/CampaignDetailPage.vue
+++ b/apps/ops/src/modules/campaigns/pages/CampaignDetailPage.vue
@@ -96,6 +96,11 @@
{{ shortLink(props.row.gift_url) }}
+
+ Voir sur Giftbit admin (vérifier statut de redemption)
+
@@ -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(() => {
diff --git a/apps/ops/src/modules/campaigns/pages/GiftsInventoryPage.vue b/apps/ops/src/modules/campaigns/pages/GiftsInventoryPage.vue
index 05dccd5..9011724 100644
--- a/apps/ops/src/modules/campaigns/pages/GiftsInventoryPage.vue
+++ b/apps/ops/src/modules/campaigns/pages/GiftsInventoryPage.vue
@@ -100,6 +100,11 @@
Copier l'URL Giftbit (à coller dans une nouvelle campagne pour réassigner)
+
+ Voir l'historique sur Giftbit (statut de redemption)
+
@@ -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' })
diff --git a/services/targo-hub/lib/campaigns.js b/services/targo-hub/lib/campaigns.js
index 57504ac..60a9067 100644
--- a/services/targo-hub/lib/campaigns.js
+++ b/services/targo-hub/lib/campaigns.js
@@ -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,