# Admin · Marketing — Logic & Flows

Canonical target = React demo `portals/admin/Marketing.tsx`. Our app = Yii2 admin (`backend/`).

The demo Marketing screen is a **3-tab segmented control**: `Deals | Ads | Push`. Only the
**Deals** tab is backed by real store logic; **Ads** and **Push** are largely static mock /
simulated UI.

Demo ref: `/private/tmp/Navagoo_MI_dev/navagoo-app/src/portals/admin/Marketing.tsx:51-160`.

---

## 1. Deals tab (the only real-logic tab)

### Demo
- Renders `<DealsManager />` with **no `shopId`** → platform-wide scope.
  Ref: `portals/marketing/Deals.tsx:17-95`.
- Tab badge count = `s.deals.filter(d => !d.shopId).length` — only platform deals (no shopId).
  Ref: `Marketing.tsx:52`.
- Table rows = `deals.filter(d => shopId ? d.shopId===shopId : !d.shopId)`.
  Ref: `Deals.tsx:21`.
- Columns: Description, Discount (`percent → "{v}%"` / `fixed → "SAR {v}"`), Usage bar
  (`usageCount/usageCap*100`), Expiry (formatted), Delete.
  Ref: `Deals.tsx:24-71`.
- **Create** (`AddDealModal`): fields description, type (percent/fixed), value, usage cap
  (default 100), expiry (default `2026-12-31`). Create disabled unless `desc && value`.
  Store action `addDeal` assigns `id` + `usageCount:0`.
  Ref: `Deals.tsx:97-180`, `store.ts:1848-1849`.
- **Delete**: confirm dialog → `deleteDeal(id)` → toast. Ref: `Deals.tsx:55-69`, `store.ts:1850`.
- Empty state when no rows. Ref: `Deals.tsx:84-90`.
- Data shape `Deal { id, shopId?, description, discountType, discountValue, usageCap,
  usageCount, expiry }`. Ref: `types.ts:497-506`.

### Ours
- Model `common\models\PromoCode` (table `promo_code`) is the real-logic equivalent.
  Columns: `code, shop_id, expiry_date, discount_value, actual_discount_value, status,
  discount_type, max_uses, uses, remaining_uses`. Const `TYPE_PERCENTAGE=1 / TYPE_FIXED_AMOUNT=0`,
  `STATUS_ACTIVE=1 / STATUS_NOT_ACTIVE=0`. Ref: `common/models/base/PromoCode.php` (@property block).
- CRUD: `backend/controllers/PromoCodeController.php:52-168` — index/view/create/update/delete,
  Tailwind layout, `PromoCodeSearch` with default sort `id DESC`.
- Index columns: #, Code, Discount Value (with `%`/SAR suffix by `discount_type`), Expiry Date,
  Status chip, Actions. Status filter only. Ref: `backend/views/promo-code/index.php`.
- Form fields: code, discount_value, discount_type, status, expiry_date (native date input,
  stored dd/mm/yyyy). Ref: `backend/views/promo-code/_form.php`.

### Logic gaps (Deals)
- **Usage tracking**: ours tracks `uses` / `remaining_uses` / `max_uses` in the model, but the
  admin **index does not render a usage bar/progress** (demo shows `usageCount/usageCap`).
  Partial.
- **`description` field**: demo deals are described by free text; ours keys off `code` only —
  no description column. Different mental model (promo *code* vs. *deal*).
- **Platform vs shop scoping in admin list**: demo admin shows **only platform-wide** deals
  (`!shopId`). Our `PromoCodeSearch` shows **all** promo codes and filters by status only —
  no "platform-wide only" filter and the **`shop_id` field is absent from the create form**,
  so a manager cannot set platform vs shop scope from the UI. Partial.
- **Create disabled-until-valid**: demo disables Create until desc+value present; ours relies
  on server-side `required` validation (`code, discount_value, expiry_date`). Equivalent outcome.

---

## 2. Ads tab

### Demo
- Pure **static array** `ADS` (3 hardcoded campaigns), no store, no CRUD.
  Ref: `Marketing.tsx:13-44, 71-99`.
- Each card: icon, status Badge (`live`→blue / else slate), name, placement, Impressions
  (`toLocaleString`), CTR `%`. **Read-only** — no create/edit/delete.
- Concepts present: campaign **name**, **placement** (App home banner / Discover carousel /
  Category top), **impressions**, **CTR**, **status** (live/scheduled).

### Ours
- Model `common\models\Ads` = **image banner only**: `image_path, image_base_url, shop_id`,
  `+ image` upload (fit 340×160). Ref: `common/models/base/Ads.php`, `common/models/Ads.php`.
- CRUD: `backend/controllers/AdsController.php:79-199` — index/view/create/update/delete with
  real image upload (`UploadAction`, Intervention resize). Index columns: #, Image, Shop, Actions
  + shop filter. Ref: `backend/views/ads/index.php`.

### Logic gaps (Ads)
- We have **real CRUD + image hosting** the demo lacks — but **none** of the demo's analytics
  concepts (impressions, CTR, placement, live/scheduled status) exist on our side. The two are
  largely **different features sharing a name**. Our Ads = uploadable banner image; demo Ads =
  campaign performance cards. Missing on both sides; treat as divergent.

---

## 3. Push tab

### Demo
- Compose form (Title, Audience text inputs) + **simulated** send → `toast.success` only.
  No real delivery, no persistence. Ref: `Marketing.tsx:101-156`.
- "Recent sends" = static array `PUSHES` (2 rows) with audience, sent count, open-rate bar.
  Ref: `Marketing.tsx:38-47, 122-153`.

### Ours
- **Real** broadcast: `backend/controllers/PushNotificationController.php`.
  - `actionIndex` compose page; `actionSend` posts `target_audience (customers|agents|all)`,
    bilingual `title_ar/en`, `message_ar/en`, optional `route`. Validates all required.
    Ref: `PushNotificationController.php:83-141`.
  - Delivery via `NotificationHelper::sendTopicNotification(...)` to FCM topics
    `TOPIC_PUBLIC_CUSTOMER` / `TOPIC_PUBLIC_AGENT`. Ref: lines 100-129.
  - `actionHistory` = **real persisted** list from `Notifications` table (topic-scoped,
    paginated 20). Ref: lines 59-78, view `backend/views/push-notification/history.php`.

### Logic gaps (Push)
- Ours **exceeds** the demo: real FCM delivery, bilingual content, audience targeting,
  persisted history. Demo's open-rate / sent-count metrics are **not** tracked on our side
  (no analytics). Functionally done; analytics absent on both (demo's are mock).
