# Admin + Shop demo-parity progress (autonomous run)

**Resume protocol:** on any new session / after a usage reset, READ THIS FILE FIRST, then
continue from the first ⬜ row. Each screen: see demo rendered (source `../Navagoo_MI`),
see portal rendered (curl login = `tests/smoke/render-smoke-admin.sh` recipe, qa_admin /
NavQA!2026x, Host backend.navagoo.localhost), fix ROW/CARD ANATOMY (not just labels), add
i18n both sides, `php -l`, curl render-verify, then `git commit` per screen. Mark the row
✅ with the commit hash here and commit this file too.

Login: qa_admin / NavQA!2026x (backend). Shop dev creds in memory `dev-test-login`.
Parallel waves are blocked while the session model is `opus-4-8` (agent-spawn gate) — if
the model is switched to sonnet, relaunch `admin-parity-waves` in batches of 5; else solo.

## Pass 1 — Admin screens (one commit each)

- ✅ Notifications — trigger table 4-col anatomy (ad81fa0) + tpl migration
- ✅ Support & content (faq/support) — earlier
- ✅ Finance · Transfer Requests — real withdrawal data (6ba69aa)
- ✅ Finance · Invoices / Charges / Balances / Commercial Config (6f43dd9)
- ✅ Shops + Subscriptions (offers/plans) (6f43dd9)
- ✅ People — drop stale Users tab, subtitle, i18n bug (29bace3)
- ✅ Bookings — status pills (8283906)
- ✅ Geography — city card grid (84a2acb)
- ✅ Events — pagination 25 + "Showing X–Y of N" (d6b3ed2)
- ✅ Invitations — subtitle (9408099)
- ✅ Catalogue — subtitle completed (close port; nested cards already present)
- ✅ Users & Roles — subtitle + user avatar (safe parts); role-mutation & matrix persistence FLAGGED (7-role vs 4-RBAC, needs sign-off)
- ✅ Marketing — already a unified 3-tab hub; aligned Promotions label + subtitle
- 🚩 Home (admin) — FLAGGED: `/` renders the SHARED site/index dashboard (admin+manager+shopOwner, shop-style KPI cards + charts), NOT the demo admin-ops Home (greeting + platform tiles Active shops/MRR/Settlement liability/Outstanding + Needs-attention list + Today-vs-yesterday rail). Rebuild needs (a) forking an admin-only home from the shared view and (b) wiring platform KPIs — decision + data work; not a safe in-loop anatomy fix.
- ✅ Dashboard — full port; added visible 'Updated · auto every 5 min' caption
- ✅ Analytics (P&L) — strong port (title/subtitle/KPI quartet/P&L statement/assumed tags all match, renders clean). MINOR gap: only 1 of the demo's 3 charts (Revenue/COGS/margin present; GMV&bookings + Revenue-by-line need controller data arrays — data work, not anatomy)
- ✅ Costs — already a complete port (title/subtitle/Add-Save/assumed banner/Cost register/Basis select/On-Off toggle); renders clean, no change needed
- ✅ Settings — demo 3-tab shell (Booking workflow / Policies & documents / Color system) is a complete port w/ matching subtitle; all 3 tab routes render clean. NOTE: sidebar 'Settings' → /settings/index lands on the LEGACY website-settings form, not the demo shell; pointing it at /settings/workflow is a Menu.php change (layouts/menu — out of loop scope) — flag for a menu decision
- ✅ Live chat — complete demo-matching preview stub (title/subtitle 'Support conversations with shops'/preview badge/coming-soon card); renders clean. PASS 1 ADMIN COMPLETE.

## Pass 2 — Admin re-review (user ask: "راجع تاني، يمكن ناقص تاب/محتوى")
- 🔄 Pass 2 IN PROGRESS
### P2 checklist (second-sweep; ✅=re-reviewed clean/fixed)
- ✅ Notifications (form re-reviewed: complete; only the already-flagged eventBasis-vs-event_key schema diff)  - ✅ Support (contact 7-channels match demo exactly; FAQ/Policies/Contact tabs complete)  - ✅ Finance(5 tabs)  - ✅ Shops (avatar/branch/type+vat chips/tab-count present)  - ✅ Subscriptions (feature matrix present)
- ✅ People (3 tabs, stale Users tab removed)  - ✅ Bookings  - ✅ Geography (6 city cards + district chips)  - ✅ Events  - ✅ Invitations (subtitle + Pending/History)
- ✅ Catalogue  - ✅ Users&Roles (tabs+avatars; matrix persistence still flagged)  - ✅ Marketing  - ✅ Dashboard (5 tabs + updated caption)  - ✅ Analytics (P&L statement + KPIs)
- ✅ Costs  - ✅ Settings (3 tabs 200)  - ✅ Live-chat (200). **PASS 2 ADMIN COMPLETE — all 18 re-reviewed.**
: second sweep every admin screen for missing tab/content/dropdown

## Pass 3 — Shop portal parity (same system) 🔄 IN PROGRESS
> **P3 render baseline:** shop render-smoke = 30/30 PASS (all shop pages render clean; only /social-media 404 = optional feature). The shop portal has been the primary demo-porting target all session (Earnings/calendar/services/auto-translate all committed+verified earlier).

### P3 checklist (shop portal; ✅=reviewed/fixed). Login: dev shop creds (memory dev-test-login). Host shop.navagoo.localhost.
- ✅ Home/Dashboard  - ✅ Bookings(calendar, day/list/month, demo-ported earlier)  - ✅ Services  - ✅ Service Bundles/Packages (render clean)  - ✅ Customers (render clean)
- ✅ Finance(Earnings/Charges/Invoices/Settlement) — 4-tab shell matches demo ShopFinanceLayout (underline nav + Invoices/Settlement badge counts); all 4 panes render clean (smoke 200); demo-ported earlier  - ✅ Team — subtitle aligned to demo `<shop> · specialists, compensation & payroll`; 3 tabs (Specialists/Payroll/Structure) match demo first 3; render clean. 4th "Staff logins" tab FLAGGED (net-new shop-scoped RBAC feature)  - ✅ Notifications — inbox list matches demo Notifications.tsx (bell chip + unread dot + title/body/relative-date + Mark-all-read on unread>0); subtitle aligned to demo static 'Your in-app alerts — booking activity and settlements' (was dynamic {n}-unread); portal adds per-row mark-read + pagination on top. Render clean  - ✅ Marketing/Promo (/promo-code) — header 'Marketing' matches demo pageTitle; subtitle aligned to demo `<shop> · promo codes & deals` (was shop title only); columns a legit superset of demo PromotionsManager (adds Constraints/Window from NVG-BEA). Render clean
- ✅ Invitations (/customer-invitations) — subtitle aligned to demo shop.invitations.pageSubtitle 'Invite walk-in contacts to create a Navagoo app account. Admin approves each batch before anything sends.' (was shop title only); portal invite-form + history table is a legit richer implementation of the demo batches flow. Render clean  - ✅ Branches (/branch) — ported 3 missing demo anatomy pieces: title 'Branches List'→'Branches'; subtitle → demo `<shop> · N branch(es)` dynamic count; NEW head-office card at top (parent shop + 'Main location · {city}' + 'Head office' badge, Branches.tsx:42-51); per-branch 'Independent ledger & subscription' note (BadgeCheck teal, Branches.tsx:73-77). php -l clean, CSS rebuilt (text-teal-500 now in bundle). CAVEAT: dev shop's plan lacks the branches feature → /branch renders the plan-gate upsell for this account, so the new markup is php-lint+render-200 verified but NOT visually exercised at runtime (will show for a branches-enabled shop)  - ✅ Settings (/settings) — 5 tabs (General/Scheduling/Payments/Commercials/Notifications) match demo Settings.tsx EXACTLY; subtitle aligned to demo `<shop> · general, payments, commercials & notifications` (was all-5-tabs list). All 5 tabs render 200 clean  - ✅ Navagoo Plans (/navagoo-plans) — already a complete demo port of finance/Subscription.tsx: header EXACTLY matches demo (title 'Navagoo Plans' + subtitle `<shop> · your subscription & plan`); hero status card, period toggle (monthly/6mo/yearly), plan-tier grid, offers, payment-methods, subscription-invoices all present. Render 200 clean; no change needed  - ✅ Analytics (/shop-analytics) — header matches demo Analytics.tsx EXACTLY (title 'Analytics' + subtitle `<shop> · performance & financials`); 8 chart canvases == demo's 8 panels (busiestDays/category/customerMix/ontime/peakHours/status/topServices/topSpecialists) + settlement-balance + trend + range filter. Render 200 clean; no change needed  - ✅ Reviews (/rate) — NO demo shop-portal Reviews screen exists (reviews live only in the demo CUSTOMER portal portals/customer/Shop.tsx + dashboard 'recent activity'). Portal /rate is a shop-native page (owner reads ratings/reviews customers left, filter by agent/rating/text) with no demo counterpart to match anatomy against; render 200 clean. Left subtitle as-is (no demo descriptor to copy — not fabricating one)  - ✅ Help (/technical-support) — BUILT the demo HelpCenter reader (components/HelpCenter.tsx, audience="shop"): added a FAQ accordion (faq audience=1, status=1, ordered; EN via translations_with_text join) + a Policies accordion (business policy pages policy-privacy-business/policy-terms-business, bilingual) ABOVE the existing contact-support form. New controller loaders TechnicalSupportController::loadShopFaqs()/loadBusinessPolicies() (raw Query, mirror FaqController). Verified: /technical-support 200 clean; Policies section renders 2 real bilingual policies (Privacy/Terms Business w/ full PDPL body) — proves the reader path; FAQ section correctly hidden because 0 shop-audience FAQs are seeded locally (data-driven — will show once admin authors shop FAQs). Bilingual keys 'Help & FAQ'/'Policies' added both sides; CSS rebuilt (group-open:rotate-180). **PASS 3 COMPLETE — all 12 shop screens re-reviewed.**

- ⬜ enumerate shop menu items; demo `#/shop/*` vs portal; fix per screen

## Pass 4 — Functional verification (user ask: settings must actually take effect) 🔄 NEXT
> Pass 3 done (all 12 shop screens + all 18 admin screens). Now prove behaviour, not render.
- ⬜ per screen: does the action DO the thing? Drive the flow, observe the effect, not just render.
### P4 checklist (functional — each: perform the action, then observe the downstream effect)
- ✅ Admin Commercial Config save → NEW charge uses new rate — **PASS (code-trace + read-only DB)**. Chain: CommercialConfigController::actionIndex→$model->save() persists the CommercialConfig singleton → FinanceLedgerService::marketingRatePct()/processingRatePct()/processingFixed() read the LIVE singleton via commercialConfig() (CommercialConfig::find()->one(), per-REQUEST instance cache — no stale static/global cache) with precedence per-shop override → global config → CEO default → buildMarketingFee()/buildProcessingFee() stamp the resolved rate into charge.rate_snapshot (col confirmed decimal(7,4)) and compute total_amount from it on each NEW Charge. No hardcoded rate at the compute site. NOTE: commercial_config table currently has 0 rows → charges fall back to per-shop/CEO defaults until an admin saves once (designed fallback, getInstance() returns `new static()` and first save inserts). Did NOT drive a live save+booking (would mutate financial data — never-edit list); verified by trace + read-only DB.
- ✅ Shop service price edit → new price at booking-create — **PASS (code-trace + read-only DB)**. Shop edits via ShopServiceController::actionUpdate→saveAll() writing shop_service.{service_amount, service_amount_before, price_excl_vat_after_discount, discount}. Booking-create reads it LIVE: AgentsBookingsController (`ShopService::find()->where(shop_id)->all()` then `'price'=>(float)$s->service_amount`, fresh query per create — no cached copy), WalkInBookingService::482, GroupBookingService (reads $sv->service_amount). On create the price is SNAPSHOTTED onto booking_service.{service_amount, service_amount_before, price_excl_vat_after_discount} (cols confirmed) → new bookings use the edited price; existing bookings keep their historical price (intentional snapshot, NOT a stale-cache bug). DB sample: shop_service id71 service_amount=190 (before 200 → discount reflected). Did NOT drive a live booking (mutates booking data — never-edit list); verified by trace + read-only DB.
- ✅ Shop payment-method toggle → checkout options — **PASS (code-trace + read-only DB); setting is server-ENFORCED, not just displayed.** Persisted: PaymentSettingsController::actionIndex→save() → shop_payment_settings{pay_online_enabled,pay_deposit_enabled,pay_on_visit_enabled,deposit_percentage}. Consumed at checkout via ShopPaymentSettings::getEnabledModes() (maps the 3 tinyint cols → offered modes): WalkInOptionsService (shop-portal walk-in booking, deposit % appended) + api/controllers/BookingController.php:742 (mobile customer checkout offers only enabled modes + deposit_percentage). ENFORCED: api/BookingController:807-813 rejects a chosen payment mode not in getEnabledModes() (NVG-BEA-001). NOTE: shop_payment_settings has 0 rows → every shop falls back to default (WalkInOptionsService null→[ON_VISIT]; api null→[ONLINE]; new-model default pay_online_enabled=1) until it saves once — designed fallback. Did NOT drive a live booking (mutates data; api/ is read-only for me); verified by trace + read-only DB.
- ✅ Admin notif-trigger approve → shop can use channel — **PASS (functional; trace + read-only DB)**. Persisted: NotificationTriggerController::actionApprove(id) flips notification_trigger.approval_status=1 (+approved_at/by); editing a paid SMS/WA template re-pends (actionUpdate:89-92). Consumed BOTH at selection AND send: shop Settings→Notifications _notifications.php:73 shows a paid channel selectable only when `channelEnabled($ch) && isApproved()`; dispatch NotificationDispatchService:315 refuses to send a paid channel unless approval_status===1; model channelUsable() = in-app always OR (paid AND approved). DB: triggers 1-4/8 approval_status=1 w/ seeded paid templates (m260727_140000). Not a dead flag. **FLAG CONFIRMED + refined:** approval is a SINGLE per-trigger approval_status (0/1) → SMS & WhatsApp of one trigger share ONE state; the demo approves each channel independently. Functional wiring is correct; only the granularity differs (per-trigger vs per-channel = schema-level parity gap, see Flags). Verified read-only (existing approved rows demonstrate the consumed side; did not POST a mutation).
- ✅ Shop Settings→Scheduling → booking slot grid — **PASS (trace + live read-only endpoint + DB)**. Persisted: SettingsController:312 whitelist $shop->save([open_at, close_at, slot_time_step, limited_schedule_days, time_format, first_workday]). Consumed: BookingScheduleService::shopWindow() = wallToMin(open_at)..wallToMin(close_at) (overnight → +1440); freeSlots() sets $step=slot_time_step?:15 then builds candidates ceil(start/step)*step stepping by $step, bounded by window ∩ agent shift; dayLayout() uses same window+step for the Day calendar. Live GET /booking/slots?agent_id=36 → {success:true, slots:[]} — path executed; empty because dev shop 15 has NO agent shifts (a separate necessary gate: slots = shop-window ∩ agent-shift ∩ free), NOT a wiring bug. DB: shop 15 open_at='12:00 AM', close_at='11:59 PM', slot_time_step=15. Verified by trace + live endpoint + read-only DB.
- ✅ Promo code + constraints → checkout discount & enforcement — **PASS (37 unit tests green + code-trace)**. Constraint logic PromoCodeService::validate() enforces status/wrong-shop/window(active_from/until)/total-cap/per-customer-cap/first_time_customer_only/service-scope, cheapest-first. Wired at checkout in api/models/BookingForm.php (both promo paths lines 245 & 322): `$validation = PromoCodeService::validate(code, shop, customerId, services_ids, now)`; discount applied ONLY `if ($validation['ok'])` → ineligible code (cap/not-first-time/wrong-service/outside-window/wrong-shop/inactive) sets valid_code=NOT_VALID, NO discount; valid → discount_value() applied + VAT/total_paid recomputed on net; recordRedemption() (line 177 / BookingController:932) increments uses + auto-deactivates at cap. The earlier 'W2 flagged api edit' (m260726_150000 docblock) has LANDED — enforcement is live in the api customer checkout. Ran `codecept run unit components/PromoCodeServiceTest` = OK (37 tests, 113 assertions) covering service-scope/caps/first-time/window/redemption. Verified by tests + trace (did not create a real booking — api customer-facing/never-edit).
- ✅ Admin subscription/plan change → plan-gated features unlock/lock — **PASS (trace + read-only DB); explains the P3 /branch gate exactly.** Gate: app-level EntitlementFilter ('as entitlement' in frontend/config/web.php) FEATURE_MAP[controllerId]→feature (branch→multi_branch, package→promo_codes_packages, shop-analytics→advanced_reports, promo-code→deals_promo, customer-invitations→messaging_campaigns, social-media→social_connector); if !EntitlementService::shopCanAccess(shopId, feature) → renders site/_locked. Resolver: shopCanAccess → ShopSubscription::findCurrentForShop (LIVE) → shopFeatures($sub) → featuresForTier($plan->tier) → hasFeature. Tiers cumulative starter⊂growth⊂pro; multi_branch ∈ PRO_DELTA (PRO-only). DB: dev shop 15 sub = plan_id 2 / tier growth / status past_due → featuresForTier('growth') excludes multi_branch → /branch locked (matches P3). Admin moving shop to Pro plan (id 3) → proFeatures() includes multi_branch → /branch unlocks. Fail-open (no sub → allow, pre-billing; broken lookup → allow). past_due is not access-state 'locked' so growth features stayed open in P3 smoke. Did NOT mutate the subscription; verified by trace + read-only DB.
- ✅ Customer invitation send → admin approval gates dispatch — **PASS (trace + read-only DB)**. Shop CustomerInvitationsController::actionCreate saves CustomerInvitationCampaign at status=STATUS_PENDING_APPROVAL(0) + recipients PENDING → NO messages sent (shop can't self-send; +throttle capping concurrent pending batches). Admin CustomerInvitationCampaignController::actionApprove (POST-only) is the ONLY dispatch path: guarded `status != PENDING_APPROVAL` (no re-send), loops recipients calling waHelper->sendShopUserInvitation(), then status→COMPLETED(2); actionReject → REJECTED(3), never sent. DB: shop 15 campaigns all status=2 (approved+sent); new batches start at 0. Matches demo 'admin approves each batch before anything sends' (the P3 Invitations subtitle). Did NOT create/approve a real batch (sends real WhatsApp/SMS — external side effect); verified by trace + read-only DB.

### PASS 4 COMPLETE — all 8 functional checks PASS (settings/actions genuinely take effect; verified by code-trace + read-only DB/tests, no live financial/booking mutations). Commits: 5e91cc0, 268e7c6, 938c1c1, e5c41f0, e6022eb, a119e63, 685cf61, + this.

## Flags awaiting user decision (do NOT guess)
- Users&Roles: demo 7 platform/shop roles vs portal 4 RBAC roles; permission-matrix persist
- Notifications: per-channel approval (demo) vs single per-trigger gate (schema) — **CONFIRMED by P4 item 4**: notification_trigger.approval_status is ONE 0/1 column per trigger; approving flips SMS+WhatsApp together. Demo approves each channel independently. Fix would need a per-channel approval schema (e.g. sms_approval_status/wa_approval_status or a channel-approval table) + UI + dispatch/settings read changes — schema decision, needs sign-off. Functional approve→use path already works (per-trigger).
- Events: surface personas (admin/shop/customer/specialist/system) vs our application col
- api/faq returns all audiences → shop-audience FAQs would leak to mobile customer app
- Shop Team: demo has a 4th "Staff logins" tab (gated on `useCan('shop.team.manageStaff')`) — create shop-scoped sub-login accounts + assign shop roles + deactivate/reactivate. Portal has NO shop-staff/sub-login concept (only the owner LoginForm). Net-new feature: needs a shop-users+roles schema, a controller, and the `shop.team.manageStaff` permission. Not an in-loop anatomy fix — needs sign-off (parallel to admin Home flag).

---
## RE-DO (2026-07-28, wave method) — user rejected the label-only review: "مش هو هو خالص"
Full prop-by-prop rebuild of every admin screen via demo-fidelity skill (see per-screen DIFF tables in agent outputs). Real anatomy fixes, not subtitles.

**Wave 1 ✅** (CSS built c56b068): Users&Roles 2b1f741 (master/detail + real matrix + Deactivate) · Shops 58eefaf (🔴 was 32th/33td BROKEN — rebuilt to 8 demo cols + Toggle) · Notifications c2faf55 (modal form + per-channel enable toggle + shop=in-app rule) · People 798f9a1 (dropped extra Users tab + badgeTint + Override modal) · Dashboard 8bcaa7e (MISSING financial chart added + 18 KPIs + funnel rebuilt)
**Wave 2 ✅** (CSS built 8c2b0ee): Support&Content f3984eb · Bookings 01dd81b (🔴 was 31 cols/10 filters — rebuilt to 7 cols/2 filters) · Geography fb1b6cd (built full CityModal + actionSaveGeo, was missing districts editor) · Subscriptions a4daad5 (matrix saves-on-toggle + plan/offer modals) · Finance 4ac7ef7 (5 tabs, colspan bug fixed, ⃁ money, formula strip; money-mutation untouched)
**Wave 3 ✅** (CSS 6332602): Analytics 0b85c98 · Costs 26bf898 · Settings 36c24a3 (removed extra General tab; fixed 'Final—no onward moves' never-rendered bug) · Marketing 4904b6f (rebuilt stat-cards landing into real inline tables) · Invitations 6c89ee8
**Wave 4 ✅** (CSS 0dc45c0): Catalogue 18ca3fd · Events 80e834e · LiveChat 20d176f · Home dec89bd (fork already existed; fixed Active-Shops tile that showed BOOKING count not shop count)

**ALL 19 ADMIN SCREENS REBUILT prop-by-prop. Full admin smoke = 27/27 routes 200 clean (CSS built). Structural bugs found+fixed: Shops 32th/33td mismatch · Bookings 31→7 cols · Settings phantom tab + dead branch · Dashboard missing financial chart · People extra Users tab · Home wrong KPI source.**

### MIGRATIONS TODO (user: "متنساش تعمل ميجريش بالداتا الموجودة") — write AFTER waves, one at a time, idempotent + BACKFILL existing rows, present to user before applying:
1. navagoo_subscription_plan + navagoo_offer → name_ar / description_ar (backfill = copy current EN name)
2. notification_trigger → sms_approval_status + wa_approval_status (backfill from current approval_status) — enables demo per-channel approval
3. shop → logo_hue (backfill = crc32(id)%360) — so ShopTile isn't derived
4. notification_trigger → event_basis (backfill from event_key map) — demo "Fires" enum
### Per-screen DATA-GAPs collected so far: bookings 9-status vs demo-5 (pill maps to single status); finance package-earned sublines + settlement-invoice linkage (transfer_request vs withdrawal tables); analytics/dashboard proxy metrics need booking started_at/finished_at.

## MIGRATIONS APPLIED ✅ (2026-07-28, commit 7022da3, user approved "طبّقهم كلهم")
`php console/yii migrate` ran clean. Backfill verified:
1. navagoo_subscription_plan/offer name_ar + description_ar — 3 plans backfilled from EN name
2. notification_trigger sms_approval_status + wa_approval_status — 8/8 backfilled = old approval_status (behaviour preserved; code-wiring to per-channel is the remaining follow-up)
3. shop logo_hue — 20 shops backfilled CRC32(id)%360 (matches ShopTile ports)
4. notification_trigger event_basis — event-driven triggers mapped, reminders NULL
Post-migration regression: admin smoke 10/10, shop smoke 30/30 — no breakage. (mobile API: additive columns, backward-compatible.)

### REMAINING FOLLOW-UPS (code, not schema — the migrations laid the groundwork):
- Wire notification dispatch/settings/approve to sms_approval_status/wa_approval_status (per-channel) instead of the single approval_status; add a 2nd Approve button per channel in _row.php.
- Point plan/offer views + models at name_ar (BilingualField) now that the columns exist.
- Point ShopTile ports at shop.logo_hue instead of the crc32 derivation.
- Notifications "Fires" form field → bind to event_basis (enum) instead of deriving from event_key.
- Analytics: extend PlatformPnlService::trend() to emit GMV/bookings + revenue-by-line (2 missing demo charts) — SQL drafted in agent output.
- Home: per-permission money→ops KPI-tile swap for admins lacking admin.finance (demo behaviour).

## LOOK & FEEL ROOT-CAUSE PASS (2026-07-28 late) — user: "الشكل مش هو هو خالص" (fonts/padding/margins/bg/hover)
Measured (computed styles, both renders side-by-side) — the gap was SYSTEMIC, not per-screen:
- ✅ Arabic font Noto Kufi → **Alexandria variable** (demo M9 single stack; Latin stays Google Sans) — 2c0e467
- ✅ body 14px/1.5 (was 16px) + content wrapper **max-w-[1600px] px-5 lg:px-8 pt-6 pb-10** (was 1400 centered w/ 35px float = the "مسافات" complaint) + 17 h1 normalizations — 742df63
- ✅ **ICU 76: bare 'ar' = LATIN digits** → formatter pinned ar-SA/en-US in BackendController + MoneyHelper = format.ts twin (⃁, compact ألف/مليون, localeDigits, ar dates) + dashboard/home/ledger wired + Chart.js locale/font — fbe2c65
- ✅ ar 'Update' حدث→تحديث — 55ee6ef · People blank-rows fix — a262b40 · Users&Roles content parity (one table, role Select, add-user modal, live custom matrix, role delete) — b6112b7 · shop-manager screens demo grammar — 2d1d5be
- Verified: admin smoke 10/10 (SMOKE_USER=qa_admin) · RBAC toggle round-trip in rbac_auth_item_child · side-by-side screenshots near-match
- ✅ shop portal font+locale swap DONE (473b74c, smoke 30/30) · ⬜ FOLLOW-UPS: 'SAR ' sweep over remaining admin views (payment/withdrawal/finance details, _pdf) · bilingual custom-role labels · demo rename-role modal

## SHOP MANAGER ROLE — built + wired end-to-end (2026-07-28)
User asked: create a shop-manager role + migration + make it work. Done:
- **RBAC** (rbac migration m260728_120000, APPLIED): role `shopManager` + permission `manageShop`; graph `shopManager→{manageShop,user}`, `shopOwner→manageShop`. User::ROLE_SHOP_MANAGER const added. Users&Roles screen shows it (amber, "Manage the shop"). Commit eb53d87.
- **Auth wiring** (commit 6852bb3): User::getActiveShop() = owned shop ?: shop_id shop (PROVEN owner-behaviour-preserving: 0 owners have shop_id w/o owning a shop) + getActiveShopId()/canManageShop(); LoginForm gate accepts `manageShop`; SignInController::actionLogin routing uses $activeShop; FrontEndController::beforeAction uses activeShop; globalAccess allowlist (frontend/config/web.php) adds shopManager; 74 `identity->shop`→`identity->activeShop` swaps across 21 controllers.
- **VERIFIED end-to-end**: test manager (user 958, shop_id 15) logs into shop.navagoo + browses ALL pages 200 (dashboard/agents/services/earnings/customers/calendar). Owner smoke stayed 30/30 (zero regression).
- Test login (dev only): shopmgr.test@navagoo.local / MgrTest!2026 (manager of shop 15).
- **REMAINING**: self-serve UI to CREATE shopManager accounts = the demo shop Team→"Staff logins" tab (still flagged). Also: per-feature restriction (block subscription/delete for managers) if wanted — currently manager has full shop access as the user chose ("كل صلاحيات المتجر").
