# Livewire Dashboard Plan

Bring the resto panel up to where `botique-backend` ended up after its
"final touches" commit (`214286f`): one dashboard per role, and the pages a
restaurant could previously only reach from the mobile app. botique was forked
from this codebase, so most of that work ports with `boutique` → `restaurant`.

Written 2026-09-28. The six reported issues are already fixed. See
[Already done](#already-done) before starting anything below.

---

## Where the panel stands today

| Area | Today | Problem |
|---|---|---|
| Admin dashboard | 8 analytics components (`DashboardOverview`, `OrderAnalytics`, `CostumerAnalytics`, …) over a 500-line `AnalyticsService` | Reports on how each restaurant runs its kitchen. The admin's actual job (approving restaurants, selling wallet top-ups, setting commission) isn't on it. |
| Restaurant dashboard | `RestaurantStats`, `RestaurantTables`, `RestaurantSubscriptions` | OK as a base. |
| Staff | Cannot log in (`Login` allows `user` + `restaurant` only) | The API supports staff; the panel doesn't. |
| Restaurant self-service | Settings only | Staff, theme, gallery, onboarding slides, wallet and open/closed exist only in the mobile app. |
| `/transactions` (admin) | Every wallet row in the system | Shows restaurant commission and driver earnings as "revenue". |
| Restaurant activation | Two overlapping components (`Restaurant\ActivateRestaurant`, `Wallet\ActivateRestaurant`) | No record of why a restaurant was refused. |
| `/users`, `/roles` | Generic CRUD | botique deleted both. Roles are seeded, not edited. |

---

## Phase 0: tenancy and access (do first)

Nothing in later phases is safe until every panel query is tenant-scoped.

1. **`Order::scopeForCurrentTenant()`**. Copy it from botique's `Order` model and
   use it in `IndexOrder`, `ViewOrder`, `AssignDriverToOrder` and `EditOrder`.
   `IndexOrder` scopes by `$user->restaurant->id` only for `type === restaurant`.
   Any other type sees every order, so this must land before staff can log in
   (Phase 1).
2. **`OrderService::paginateForPanel()` + `panelStats()`**. Move the query out
   of `IndexOrder::render()` (botique has both). This also fixes the date
   filter: `whereBetween('created_at', [$from, $to])` drops the whole last day.
3. **`/settings` route**. Its `permission:edit settings` middleware is commented
   out. Split the page (Phase 2, item 1) so the admin page can be gated again.
4. **Tests to port**: `PanelAccessTest`, `PanelRoutesSmokeTest`,
   `PanelOrderServiceTest` and `ModalCancelTest` from botique. Renaming
   `boutique` → `restaurant` gets most of them passing.

## Phase 1: one dashboard per role

`resources/views/pages/dashboard.blade.php` branches on user type:

| Role | Component | Content |
|---|---|---|
| admin (`user`) | `Dashboard\AdminOverview` (new) | Restaurants awaiting review, per-restaurant wallet balance, top-up revenue, who is about to run dry. Replaces the 8 analytics components. Delete `AnalyticsService`. |
| restaurant | `RestaurantStats` + `RestaurantTables` (keep) | Add the live order countdown to `RestaurantTables` (`<x-order-countdown>`, already built). |
| staff | `Dashboard\StaffQueue` (new) | The three live order states and the orders waiting on someone. Requires Phase 0 item 1, then allow `UserTypesEnum::STAFF` in `Login`. |

Test to port: `AdminOverviewTest`.

## Phase 2: restaurant self-service pages

Each page resolves its tenant with `Tenant::owned()`, never from the URL, and
gets a restaurant-scoped permission in `PermissionForRestaurantSeeder`.

| # | Page | Route | Wraps (already exists) |
|---|---|---|---|
| 1 | `Setting\EditRestaurantSettings` | `/restaurant-settings` | `SettingsService` dispatch/pricing/timeouts (incl. `driver_offer_timeout`). `EditSettings` becomes admin-only: app versions + default commission. |
| 2 | `Staff\IndexStaff`, `Staff\CreateStaff` | `/staff` | `RestaurantUserService` (`POST /restaurants/staff/assign`) |
| 3 | `Theme\EditTheme` | `/theme` | `ThemeService`. Replaces `Setting\Themes\*`. |
| 4 | `Gallery\IndexGallery` | `/gallery` | `RestaurantGalleryController` logic, plus reorder (botique `moveGalleryImage()`) |
| 5 | `Onboarding\IndexOnboarding` / `Create` / `Edit` | `/onboarding` | `RestaurantOnboardingController` logic |
| 6 | `Restaurant\MyWallet` | `/my-wallet` | Balance + ledger. Owner-only: withhold from staff. |
| 7 | `Components\AvailabilityToggle` | top bar | `PATCH /restaurants/availability`. Renders nothing for the admin. |
| 8 | `Profile\ViewProfile` | `/profile` | Call `RestaurantService::updateRestaurantProfile` so cover image, social links and map pin stop being dropped; deprecate `AuthService::updateRestaurantProfileWebSide`. |

Tests to port: `StaffManagementTest`, `BoutiqueContentTest`,
`BoutiqueSettingsTest`, `SettingsOwnershipTest`, `PanelProfileTest`.

## Phase 3: admin money and approval

1. **`Restaurant\ReviewRestaurant`**: one approve/refuse screen replacing both
   `ActivateRestaurant` components. Needs a `RestaurantStatusEnum` and a
   migration adding `status`, `rejection_reason`, `reviewed_at`, `reviewed_by`
   (botique `BoutiqueStatusEnum`). Test: `BoutiqueApprovalTest`.
2. **`Wallet\AdjustRestaurantWallet`**: manual credit/debit, each writing a
   ledger row. Test: `WalletAdjustmentTest`.
3. **`Revenue\PlatformRevenue`** at `/revenue`: replaces `/transactions`, counts
   top-ups only. Needs `PlatformRevenueService`. Test: `PlatformRevenueTest`.
4. `Restaurant\SetRestaurantCommission` is **done**.

## Phase 4: order operations in the panel

1. `ViewOrder` accept/decline should go through `OrderStatusService` (same
   transitions, notifications and broadcasts as the app). The countdown is
   already on it.
2. `AssignDriverToOrder` should offer through `DispatchService::offerToDriver`,
   so the `driver_offer_timeout` withdrawal and the driver's ring both apply.
3. **Client cancel window** (botique `client_cancel_window` +
   `POST /orders/{id}/cancel`). The client app's "Cancel order" currently calls
   the restaurant's `/decline` endpoint, which clients have no permission for,
   so it always fails. Needs a migration, `OrderCancelReasonEnum::CLIENT_CANCELLED`,
   a route and the app's `cancelClientOrder`.

Test to port: `PanelOrderFlowTest`.

## Phase 5: cleanup (needs decisions)

| Item | botique did | Decide for resto |
|---|---|---|
| Driver subscriptions | Deleted (`CheckDriverSubscriptions`, export, PDF, `latestSubscription`) | Keep or drop? If kept: the daily `app:check-driver-subscriptions` deactivates any active driver whose *latest* subscription has expired, so a driver reactivated by hand without a new subscription is switched off again the next day. |
| `/users`, `/roles` pages | Deleted | Drop? |
| `IndexTransactions` | Replaced by `/revenue` | Drop after Phase 3. |
| Translations | `TranslationKeyTest` fails on any `__('app.x')` with no key | Port it. |

---

## Already done

Delivered with this plan (backend + mobile app):

| # | Issue | Fix |
|---|---|---|
| 1 | Panel "Activate" only showed for drivers with a live subscription | Gate removed (`ActivateDriver`). Toggle shown for `edit driver` (what restaurants hold) as well as `activate driver`, scoped to the tenant. Driver list scoped to the tenant. |
| 2 | "Leave a review" stayed after reviewing | `reviews.order_id` (migration), `OrderResource.is_reviewed`, one review per order per target; app hides the button. |
| 3 | Restaurants set their own commission | `order_revenue` is admin-only: platform default 10% (Settings page), per-restaurant override (restaurants list → %). Charged on `sub_total`, not `total`. `delivery_revenue` dropped. |
| 4 | Order-flow timeouts not shown | `Order::cancellationDeadline()` shared by the sweep and every timer; `OrderResource.deadline`; `expires_at` on every ring; `driver_offer_timeout` + offer expiry sweep; countdown in client status, order cards, restaurant sheet, ringing screen and the panel. |
| 5 | `vehicle_id` | Renamed to `driver_license` (migration); licence image shown and replaceable in the driver's personal info; update route restricted to the driver themselves; licence never sent inside order payloads. |
| 6 | Driver could accept/refuse at order placement | Root cause: a device's push token stayed on the previous account (restaurant) after signing in as a driver, and the app's logout never reached the backend. The token is now detached on register and cleared on logout, the logout URL is fixed, and the ringing screen refuses rings meant for another role. |

Tests: `tests/Feature/OrderFlowFixesTest.php`, `tests/Feature/PanelFixesTest.php`.
