# Admin Parity Audit — Notifications / Marketing / Invitations (demo v0.28 → portal)

Sources audited (read-only):

- Demo: `/Users/ahmedqotb/Documents/dockermachine/www/Navagoo_MI/navagoo-app/src/portals/admin/{Notifications,Marketing,Invitations}.tsx`, `src/portals/marketing/Promotions.tsx`, `src/i18n/en.ts`, `src/portals/nav.ts`, `src/components/ui/Badge.tsx`, `src/types.ts`
- Portal: `backend/views/notification-trigger/{index,_row,_form}.php`, `backend/controllers/NotificationTriggerController.php`, `common/models/NotificationTrigger.php`, `backend/views/marketing/index.php`, `backend/controllers/MarketingController.php`, `backend/views/{ads,push-notification,promo-code}/index.php`, `backend/views/customer-invitation-campaign/index.php`, `backend/controllers/CustomerInvitationCampaignController.php`, `backend/views/layouts/{_tw_admin_sidebar.php,menu/Menu.php}`

---

## 1. NOTIFICATIONS (demo `Notifications.tsx` → portal `notification-trigger/*`)

### (e) Sidebar / route — MATCHES

`backend/views/layouts/menu/Menu.php:116-122`: sidebar "Notifications" → `/notification-trigger/index` (active for both `notification-trigger` and `push-notification` controllers). This is correct — the sidebar points at the triggers catalogue, NOT the push composer. No top gap here.

### (a) Sections & structure

| Demo | Portal | Verdict |
|---|---|---|
| Two tables: Customer / Shop | Two tables: Customer / Shop | match |
| Add/Edit happens in a **modal** (`Modal size="lg"`) on the same page | Full-page navigate to `create.php` / `update.php` (`_form.php`) | **layout gap** — flow parity kept, interaction pattern differs |
| Row count badge per section: none | Portal adds a count pill next to each section title | superset (minor visual drift) |

### (b) Column / label / copy differences

| Item | Demo (en.ts `admin.notifications.*`) | Portal (`Yii::t('backend', …)`) | Gap? |
|---|---|---|---|
| Page title | `title: 'Notification triggers'` | `'Notification Triggers'` | casing only |
| Subtitle | `'The catalogue of automated alerts. SMS/WhatsApp templates need provider approval before shops can use them.'` | `'Catalogue of automated notification events. SMS/WhatsApp channels will need provider approval before shops can activate them.'` | **copy drift** (paraphrase, not the demo string) |
| Add button | `addTrigger: 'Add trigger'` | `'New Trigger'` | **label mismatch** |
| Section 1 | `customerSection: 'Customer notifications'` | `'Customer notifications'` | match |
| Section 2 | `shopSection: 'Shop notifications (free in-app)'` | `'Shop notifications (in-app)'` | **"free" dropped** |
| Columns | `Notification \| Channels & template status \| Shop config \| Actions` (4 cols) | `Notification \| Timing \| Channels & approval \| Optional \| Active \| Actions` (6 cols) | **structural**: demo folds timing into the NOTIFICATION cell (`timingLabel()` under the name) and has a single SHOP CONFIG column; portal splits into Timing + Optional + Active columns. Col-2 header `'Channels & template status'` vs `'Channels & approval'`. |
| NOTIFICATION cell | name + trigger description (`basis.*`, e.g. "When a booking is confirmed") + quoted template preview | name EN + name AR + `event_key` code chip + quoted preview | **missing the human trigger-description line** (see event-basis gap below); portal shows raw `event_key` instead — demo never shows the key in the table |
| In-app chip | `freeBadge: 'Free'` | `'Free (in-app)'` | label drift |
| SHOP CONFIG values | `shopPicksChannel: 'Shop picks channel'` / `inAppOnlyLabel: 'In-app only'` | same strings under the Optional toggle | match |
| Inactive rendering | `inactiveBadge: 'Inactive'` badge + row `opacity-60` | row `opacity-60` + Active toggle column; no "Inactive" badge | minor |

### (c) Interactions

| Interaction | Demo | Portal | Verdict |
|---|---|---|---|
| Per-channel Approve | **Per channel**: each SMS/WhatsApp row has its own `status` (`approved`/`pending`) and its own inline `Approve` button (`approveNotifTemplate(triggerId, channel)`) | **Per trigger**: single `approval_status` gate on the whole trigger (`NotificationTrigger::isApproved()`, `_row.php` comment admits it); one Approve button in the ACTIONS column, both paid channels flip together | **TOP GAP** — you cannot have WhatsApp "Pending" while SMS is "Approved" in the portal |
| Approve button placement | inline next to the pending channel chip (`admin.notifications.approveButton: 'Approve'`, tooltip `'Demo only — simulates provider template approval'`) | in the Actions column | structural |
| Un-approve (return to pending) | not in demo — editing an approved template auto-re-pends it | explicit `rotate-ccw` "Return to pending" button with shop-count confirm | superset |
| Optional toggle in table | yes (SHOP CONFIG column) | yes | match |
| Active toggle in table | no (modal only) | yes (own column, AJAX) | superset |
| Delete confirm | `'Delete "{{name}}"? Shops’ settings for it are removed too.'` via demo confirm | `'Delete trigger "{name}"? Shops\' settings for it will be removed too.'` via ngConfirm | match |
| Re-approval warning on edit | confirm before save: `'Editing the {{channels}} template pauses this alert for {{count}} shop(s) until the provider re-approves it.'` + `'Save & re-submit'` | `_form.php` carries `data-approved-edit` + `data-paused-shops` and warns via JS | ported |

### (d) Form/modal field parity (`_form.php` vs demo modal)

- Present in both: bilingual name, key (auto-slug), audience select, timing offset vs calendar (days-before + at-time, 22:00 default), per-channel bilingual templates, variable-token insert buttons, live preview, chars + SMS-segment counter, Shop-configurable toggle, Active toggle ('Master switch — inactive triggers never fire.' — verbatim match), shop-only → in-app-only channel order (`channelOrder()` mirrors demo).
- **Missing: the "Fires" event-basis select.** Demo has `eventBasis` with 6 options (`on_booking_confirmed`, `on_booking_cancelled`, `on_payment`, `on_settlement`, `time_before_appointment`, `on_invitation`) and the table row shows its label. The portal model has no `event_basis` column at all — only `event_key` (free text) + timing mode. Consequence: no "When a booking is confirmed"-style description anywhere, and timing fields are always shown (demo only shows them when basis = `time_before_appointment`).
- **Channel enable/disable**: demo has an explicit per-channel `enabled` Toggle; portal infers "enabled" from template body being non-empty (`channelEnabled()`). No way to keep an authored template but disable the channel.
- Portal timing offset accepts negative minutes with hint `'Negative = before the event. –60 = 1h before, –1440 = 24h before.'`; demo `minutesBeforeHint: '180 = 3 hours before'` (positive = before). Semantics inverted vs demo copy.

---

## 2. MARKETING (demo `Marketing.tsx` → portal `marketing/index.php` + satellites)

### (e) Sidebar / route

`Menu.php:137-143`: "Marketing" → `/marketing/index`, active for `marketing` + `ads` controllers. Demo nav: single Marketing entry. Portal matches at nav level (Ads/Push/Deals do not appear as separate sidebar items — they are reached from the hub). Good.

### (a) Tabs & structure — the biggest gap

Demo: ONE page, three tabs whose **content is inline and fully functional**:

- `tabPromotions: 'Promotions ({{n}})'` → embeds `PromotionsManager` (full table + "New promotion" modal editor)
- `tabAds: 'Ads ({{n}})'` → inline ad-campaign card grid + "New ad" modal (create/edit with 16:9 cropper)
- `tabPush: 'Push'` → inline compose form + "Recent sends" side panel

Portal (`marketing/index.php` + `MarketingController`): the hub is a **read-only landing** — each tab shows real stat cards + a "Manage Deals / Manage Ads / Compose / History" deep-link button to a separate CRUD page (`/promo-code/index`, `/ads/index`, `/push-notification/index`, `/push-notification/history`). The controller docblock says this is deliberate. So:

- **Gap (structural, user-visible):** every action is one extra navigation away; nothing is manageable from the Marketing page itself. Demo user creates a promotion/ad/push without leaving `/admin/marketing`.
- Tab badges: demo shows counts on **Promotions and Ads** (`Promotions (2) | Ads (3) | Push`); portal shows a count on **Deals only** — Ads has no badge.

### (b) Column / label / copy differences

| Item | Demo | Portal | Gap? |
|---|---|---|---|
| Title | `'Marketing'` | `'Marketing'` | match |
| Subtitle | `'Platform promotions, ads & push notifications'` | `'Platform deals, ads & push notifications'` | **"promotions" → "deals"** |
| Tab 1 label | `'Promotions ({{n}})'` | `'Deals'` (+count) | **terminology mismatch** ("Deals" is used demo-side only for the auto-applied kind; the tab is "Promotions") |
| Promotions table | `Promotion \| Discount \| Usage \| Status \| Expires` with usage progress bar (`312/1000`), row subtitle `'Code WELCOME25 · new customers'` / `'Auto-applied deal · up to ⃁50 · 1/customer'` | not on the marketing page at all — lives at `/promo-code/index` ("Deals" overview, has usage-vs-cap, shop column, status, deactivate) | table exists elsewhere (superset with Shop column/filter) but **not under Marketing → Promotions tab** |
| "New promotion" button | `marketing.promotions.newPromotion: 'New promotion'` on the tab | create lives in promo-code CRUD | navigation gap |
| Ads cards | inline cards with Impressions / Clicks / CTR + edit/delete + `'New ad'` modal | `/ads/index` was re-skinned to the same 3-up campaign-card grid (per its header comment) but create/update are separate pages, not a modal | mostly ported, one page removed |
| Push compose | audiences `customers / shops / all` = `'All customers' / 'All shops' / 'Everyone'`; bilingual title+message; deep link `navagoo://home`; `'Send push'`; side card `'Recent sends' / 'Broadcast history'` | `/push-notification/index`: audiences `customers / agents(Specialists) / shops / all` (superset: Specialists), same bilingual fields + route + phone preview (superset); history is a **separate page** `/push-notification/history`, not a side-by-side card | superset + layout gap (recent sends not adjacent to compose) |

### (c) Interactions missing on the Marketing page itself

- New/edit promotion modal (discount type, usage cap, expiry, weekday window, customer-type targeting — full `PromotionsManager` editor).
- New/edit ad modal incl. bilingual campaign name, placement select (`'App home banner' / 'Discover carousel' / 'Category top'`), scope Platform-vs-shop combobox, 16:9 image cropper.
- Send-push with inline validation banner (`'Add the {{fields}} to send.'`).
All exist in some form on the satellite pages; none exist on `/marketing/index`.

### (d) Layout

Demo segmented control swaps content in place; portal tabs also swap in place (JS) but the panels are stat-summaries. Portal adds `?tab=` history.replaceState (nice URL-state superset).

---

## 3. INVITATIONS (demo `Invitations.tsx` → portal `customer-invitation-campaign/*`)

### (e) Sidebar / route — MATCHES

`Menu.php:153-156`: "Invitations" → `/customer-invitation-campaign/index`. Demo: `nav.invitations` → `/admin/invitations`. Match.

### (a) Sections

| Demo | Portal | Verdict |
|---|---|---|
| `pendingApprovalTitle: 'Pending approval'` | `'Pending Approval'` | match (casing) |
| `historyTitle: 'History'` | `'History'` (+ pager, pageSize 50) | match, pager is superset |
| No stat cards | 3 stat cards (Total Campaigns / Sent Invitations / Failed Invitations) | superset |
| Page subtitle `'Review and approve shop-initiated customer-invite batches before they can be sent.'` | **no subtitle rendered at all** | **copy gap** |

### (b) Column / label / copy differences

| Demo (`admin.invitations.*`) | Portal | Gap? |
|---|---|---|
| `colShop: 'Shop'` | `'Shop'` | match |
| `colChannel: 'Channel'` | `'Communication Type'` | **label mismatch** |
| `colRows: 'Rows'` | `'Recipients Count'` | **label mismatch** |
| `colStatus: 'Status'` / `colActions: 'Actions'` | same | match |
| Batch id line: `date · CIB-1001` | `datetime · #<id>` | cosmetic (no CIB prefix concept) |
| Channels: `sms \| whatsapp` | SMS / WhatsApp / **Email** + a language chip (Arabic/English) | superset |
| Status chips: 5-state `pending_approval / approved / sent / registered / rejected` with per-status counts (`badge.invitationCount`, e.g. "3 Pending approval", "1 Sent 1 Registered") | 3-state delivery chips only: `Delivered / Failed / Pending` | **TOP GAP — no "Registered" state.** The demo's funnel end-point (invitee actually registered) has no portal equivalent; "Approved (awaiting shop send)" also doesn't exist (see flow below). Chip wording differs too ("Delivered" vs "Sent"). |
| Empty states: `'Nothing awaiting approval — new shop invite batches will appear here.'` / `'No decided batches yet.'` | `'Nothing to approve right now.'` / `'No processed campaigns yet.'` | copy drift |
| Unnamed row: `'Unnamed contact'` + name shown when present | portal drill-down shows **mobile number only**, never the contact name | minor gap (recipient name not displayed) |

### (c) Interactions

| Interaction | Demo | Portal | Verdict |
|---|---|---|---|
| Row expand → per-recipient list | chevron, name+mobile+status chip per row | same pattern (chevron, mobile+chip) | ported |
| Approve | confirm `'Approve {{count}} invite(s)?'` body `'The shop can send these once approved — a paid channel over the free monthly allowance raises a charge.'`; approving marks rows `approved` — **the shop sends later** | confirm `'…approve this campaign and dispatch all messages?'`; `actionApprove` **immediately dispatches WhatsApp** via `WhatsAppHelper` and marks campaign COMPLETED | **behavioral divergence**: admin-sends vs shop-sends-after-approval; also no billing/allowance mention |
| Reject | confirm `'Reject {{count}} invite(s)?'` / `'Rejected rows will not be sent. This cannot be undone.'` | confirm `'Are you sure you want to reject this campaign?'` | ported, copy drift |
| Confirm dialogs | styled `confirm()` | `data-confirm` → native Yii confirm (aurora pages should use ngConfirm per project rule) | **interaction-quality gap** (and per-memory, backend Swal/native confirms were supposed to be swept to ngConfirm) |
| Approve/Reject buttons | labeled `Approve` / `Reject` buttons with icons | icon-only ✓/✗ buttons (title attr only) | visual gap |
| View page | none (expand only) | separate `view.php` + Resend-failed action | superset |

---

## Superset features (portal-only, NOT gaps)

- Notifications: Active + Optional AJAX toggles directly in the table; explicit "Return to pending" (un-approve) with affected-shop count; section count pills; AR name shown in row.
- Marketing: real headline metrics per tab (platform deals count/redemptions, live ads, impressions, CTR, push sends/last-sent); `?tab=` URL state; push audience "Specialists"; push phone live preview; Deals overview with Shop column/filter + Deactivate.
- Invitations: Email channel + language chip; stat cards; History pagination; campaign View page; Resend-failed flow; flash summaries with sent/failed counts.

---

## Ranked gap list (by user visibility)

1. **[Marketing] Hub is a link-farm, not the demo's working screen** — Promotions/Ads/Push tabs contain only stats + "Manage…" buttons; demo manages everything inline (table, card grid, compose form, creation modals) on one page.
2. **[Notifications] Approval is per-trigger, not per-channel** — demo shows independent SMS "Approved" / WhatsApp "Pending" chips with an inline Approve per channel; portal has one gate flipping both.
3. **[Invitations] Missing the 5-state invite lifecycle** — no `Registered` (or `Approved`-awaiting-send) chips; only Delivered/Failed/Pending; and approve = admin dispatches immediately instead of demo's "shop can send once approved" model (charge warning absent).
4. **[Notifications] No event-basis ("Fires") concept** — table rows lack the "When a booking is confirmed"-type description (raw `event_key` chip shown instead); form lacks the 6-option Fires select; timing fields always visible.
5. **[Marketing] "Deals" vs "Promotions" terminology** — tab + subtitle diverge from demo (`'Promotions ({{n}})'`, `'Platform promotions, ads & push notifications'`); Ads tab count badge missing.
6. **[Notifications] Table shape** — 6 columns vs demo's 4 (`Notification | Channels & template status | Shop config | Actions`); timing should live under the name; header copy differs (`Channels & approval`).
7. **[Invitations] Column labels** — `Communication Type`/`Recipients Count` vs demo `Channel`/`Rows`; page subtitle missing; Approve/Reject are icon-only vs labeled buttons.
8. **[Notifications] Add/Edit is a full page, not a modal**; button says `New Trigger` vs `Add trigger`; per-channel enable toggle missing (enabled == non-empty template).
9. **[Invitations] Native `data-confirm` dialogs** on Approve/Reject instead of ngConfirm (violates the aurora-dialog rule and looks nothing like the demo confirm).
10. **[Marketing] Push compose and Recent sends split across pages** — demo shows compose + broadcast history side by side; portal history is a separate route.

Copy-drift roll-up (worth a bilingual i18n pass if fixed): Notifications subtitle, `Free` vs `Free (in-app)`, shop section "(free in-app)", Invitations empty-states, all Marketing tab labels.
