Wizard: gift_expiry_days now lives behind a preset toggle
(15/30/60/90/180 + Custom) instead of a naked number input. Operator
clicks a chip; the value flows back into the existing campaign param.
Inventory page (/campaigns/gifts):
- Cross-campaign view of every wrapper token with status taxonomy
(active / redeemed / expired / revoked / pending). Each card on
the counters strip is a click-to-filter shortcut.
- "Réassignables" highlighted in amber when > 0 — these are gifts
whose wrapper expired or was revoked but the Giftbit URL is still
unredeemed, ready for a fresh recipient.
- Search across name/email/url/token; per-status and per-campaign
filter dropdowns.
- One-click copy on the Giftbit URL with a tailored toast that walks
the operator through the reassignment workflow (paste into manual-
add dialog of a new campaign).
- Revoke action with confirmation; explicit about what survives
(the Giftbit URL stays valid on their side) vs what changes (our
wrapper stops redirecting).
Backend:
- GET /campaigns/gifts flattens every recipient with a gift across
every campaign — single-shot, no pagination yet (we're under 10k
gifts total).
- POST /campaigns/:id/recipients/:row/revoke sets gift_revoked=true
and broadcasts the recipient-update SSE event.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>