Offers
Winery promos with imagery
Overview
Same DataTable chrome. Columns: WINERY, TITLE, CODE (a small mono uppercase code), WINDOW (validFrom to validTo as ISO dates), ACTIVE (Yes/No). The +OFFER modal has fields for winery, title, body, redemption code, validity range, an optional imageUrl + attribution name + URL (Unsplash-style), and an active toggle.
How it works
`GET /admin/offers` returns every offer regardless of status — admins want to see drafts and archived ones too. The mobile app's `/api/passenger/offers` filters by isActive AND validity window.
Image upload is currently URL-only — the dispatcher pastes a URL (Unsplash, partner CDN), and the modal previews it. The API stores imageUrl, imageAttribName, imageAttribUrl so the rider app can render the attribution.
Code is uppercased on submit for consistency — the rider app expects mono uppercase, and accepting mixed case here would be inconsistent across the catalog.
Validity range uses two HTML date inputs that store ISO timestamps. The default is today through one week from today.
Delete actually deletes — offers don't have history we want to keep, and dropping a stale offer is the right cleanup.
Key decisions
Image URL field, not direct upload
We don't run an S3 bucket yet. Most partner imagery comes from Unsplash or the winery's own CDN, and pasting a URL is fast. If/when we add direct upload, the field swaps for a dropzone — but the data shape on the offers row doesn't change.
Codes are uppercase by force
A dispatcher typing 'plaza-dinner' shouldn't make the rider's app render mixed case. We toUpperCase on submit, and we render the codes mono uppercase across the catalog. One small policy, zero ambiguity downstream.
