# 08 — RM Requests

## Overview

The intake queue for **Relationship Manager (RM)** requests. Clients on the public Client app can request an RM-led booking — those requests land in `client_rm_requests` and are surfaced here for the RM team to action.

**Status:** Live

Each request shows a **composed status badge**: while no event exists the row shows `rmStatus($row->status)` (only codes 0 "Pending" or 1 "Chat Initiated" are ever written for RM-created flows — see Path C below); once an event exists the row instead shows `eventConfirmationLabel($event->confirmation_status)` (draft / pending / confirmed / rejected / expired). Payment status (Failed / Pending / Received) and category render alongside. The RM clicks through to a detail page where they either chat with the client or "Create Event" (which jumps to Feature 09).

### Path C — split lifecycle (IMPORTANT)

`client_rm_requests.status` (int) is treated as a **chat-lifecycle column only** for RM-created events. Its lifecycle is now `0 → 1` and then it stops moving — once an event row exists, the *event* lifecycle lives entirely on `events.confirmation_status` (string enum). This means:

- Old codes 2-5 (`Event Created`, `Quoted`, `Closed`, `Rejected`) on `client_rm_requests.status` are **no longer written by Feature 09** — `storeEvent` does NOT touch `client_rm_requests.status` anymore. The values are kept in `rmStatus()` for legacy rows and for non-RM-created flows.
- The dashboard label composer (`rmDashboardLabel`) hides this seam from the UI: callers always go through it instead of calling `rmStatus()` directly.
- The MVP / `RmChatController::createConverstaion` in VisoAPI consequently no longer needs a "don't downgrade from 9 to 1" guard — it sets status=1 unconditionally on chat init.

---

## User Stories

| ID | As a | I want to | So that |
|----|------|-----------|---------|
| RMR-01 | RM | See a paginated list of all RM requests in MY country | I work only on my territory |
| RMR-02 | RM | See per-request status & payment status badges | I know what to action |
| RMR-03 | RM | Distinguish "Included In Event" vs "Direct Invited" requests | I can prioritise |
| RMR-04 | RM | See the linked event's human ID and name once one exists | I navigate to the event flow |
| RMR-05 | RM | Open a request detail page | I see client details + chat thread |
| RMR-06 | RM | "Create Event" from the detail page | I produce a bookable event on behalf of the client |

---

## Screens & Flows

```
        Login (admin, role=8) ──▶ getRedirection() ──▶ /rm/rm-requests
                                                          │
                                                          ▼
┌──────────────────────────────────────────────────────────┐
│ /rm/rm-requests       rm/index.blade.php                 │
│  ┌──────────────────────────────────────────────────┐    │
│  │ DataTable: id | status | payment | type |        │    │
│  │ created | category | event_id | event_name | act │    │
│  └──────────────────────────────────────────────────┘    │
└──────────────────────────┬───────────────────────────────┘
                           │ DataTable AJAX
                           ▼
                ┌─────────────────────────────────────┐
                │ /rm/ajaxGetRmData                   │
                │ → RmModel::where(country)           │
                │   .with(getEventDetails,            │
                │         categoryDetails,            │
                │         clientDetails)              │
                │   row.status column rendered via    │
                │     rmDashboardLabel($row)          │
                └─────────────────────────────────────┘

Row "View Details" ──▶ /rm/rm-request-details/{uuid}     rm/details.blade.php

  ┌────────────────────────────────────────────────────────┐
  │ Tabs:                                                  │
  │   - Client info (clientDetails relation)               │
  │   - Category info                                      │
  │   - Event info (if record_type='event')                │
  │     → list of event_services                           │
  │   - Conversation (rm_conversation row, if any)         │
  │   - Status badge: rmDashboardLabel($rmDetails)         │
  │   - Action buttons (state-driven, see Feature 09):     │
  │       no event + chat exists   → [Create Event]        │
  │       event = draft            → [Send Confirmation]   │
  │                                  [Edit Draft]          │
  │       event = rejected/expired → [Edit and Resend]     │
  │       event = pending/confirmed → (no buttons)         │
  └────────────────────────────────────────────────────────┘
```

### Routes & Actions

| Route | Method | Handler | Description |
|-------|--------|---------|-------------|
| `/rm/rm-requests` | GET | `RmController::rmRequests()` | List page |
| `/rm/ajaxGetRmData` | GET | `RmController::getRmData()` | DataTable JSON (scoped by `country_code`) |
| `/rm/rm-request-details/{uuid}` | GET | `RmController::rmRequestsDetails()` | Detail page (lookup by UUID) |
| `/rm/create-event/{uuid}` | GET | `RmController::createEventForm()` | See Feature 09 |
| `/rm/store-event` | POST | `RmController::storeEvent()` | See Feature 09 |

---

## Data Model

### `client_rm_requests`

```php
// App\Models\RmModel → table 'client_rm_requests'
// Relations:
//   getEventDetails  → hasOne EventModel on uuid → record_id
//   categoryDetails  → hasOne CategoryModel on id → category
//   clientDetails    → hasOne Users on id → client_id

{
  id:               int,
  uuid:             string,              // primary lookup key
  request_human_id: string,              // displayed
  client_id:        int,                 // FK users.id
  rm_id:            int|null,            // FK admins.id
  status:           0|1|2|3|4|5,         // see rmStatus() helper
  payment_status:   0|1|2,               // failed / pending / received
  record_type:      'event'|'direct'|...,
  record_id:        string|null,         // UUID of an `events` row (when record_type='event')
  event_id:         string|null,         // same UUID, sometimes used
  category:         int,                 // FK categories.id
  country_id:       int,
  state_id:         int|null,
  city_id:          int|null,
  budget:           decimal|null,
  created_at:       datetime,
  updated_at:       datetime,
}
```

### `rm_conversation`

```php
// App\Models\RmConversation → table 'rm_conversation'
// constructor auto-generates uuid via Webpatser Uuid

{
  id, uuid,
  client_id, rm_id, rm_request_id,
  // other columns as written by API / chat service
}
```

### Status Helpers (in `app/Helpers/GlobalMethods.php`)

```php
// rmStatus($int) → coloured HTML span   (UNCHANGED from baseline; covers legacy values)
0 → Pending (red)
1 → Chat Initiated (orange)
2 → Event Created (blue)         // legacy — no longer written for RM-created events
3 → Quoted (orange)              // legacy
4 → Closed (green)               // legacy
5 → Rejected (red)               // legacy
default → Unknown (gray)

// NEW: eventConfirmationLabel($status) → coloured HTML span
'draft'     → Draft (gray)
'pending'   → Pending Confirmation (orange)
'confirmed' → Confirmed (green)
'rejected'  → Rejected (red)
'expired'   → Expired (red)
default     → "—" (muted)

// NEW: rmDashboardLabel($row) → composer
//   if $row->getEventDetails exists → eventConfirmationLabel($row->getEventDetails->confirmation_status)
//   else                            → rmStatus($row->status)
// This is the ONLY status helper that views/datatables should call directly.

// paymentStatus($int)
0 → Failed (red)
1 → Pending (orange)
2 → Received (green)
```

---

## Validations & Business Rules

| Rule | Detail |
|------|--------|
| Country scoping | `RmModel::where('country_id', $user->country_code)` — the admin's `country_code` MUST be set or the list is empty |
| Sort | `orderBy('created_at', 'desc')` |
| Detail lookup | Uses `uuid` not `id` |
| `getRmData` eager-loads | `with('getEventDetails', 'categoryDetails', 'clientDetails')` so each DataTable row can compose its status via `rmDashboardLabel($row)` without N+1 |
| Detail page eagerly loads | `with('getEventDetails', 'categoryDetails', 'clientDetails')` |
| `record_type == 'event'` only loads event details | Other types just show "Direct Invited" |
| Conversation lookup is scoped triple-way | `client_id` × `rm_id` × `rm_request_id` |
| Detail eventServices loaded when event exists | `$eventDetails->getEventServicesDetails($eventDetails->id)` |
| Pagination | DataTable `start` + `length`, no search/order pass-through to model |

---

## API Endpoints

| Method | Path | Auth | Request | Response | Consumer |
|--------|------|------|---------|----------|----------|
| GET | `/rm/rm-requests` | session + permission 8 | — | HTML | Browser |
| GET | `/rm/ajaxGetRmData` | session (AJAX whitelisted) | `draw, start, length, filter=...` | DataTable JSON | List page |
| GET | `/rm/rm-request-details/{uuid}` | session + permission 8 | path uuid | HTML | Browser |

---

## Chat Panel Rendering (`public/assets/custom/js/rm.js`)

The detail page's chat panel is now message-type aware. `getMessages` returns each row with a `message_type` discriminator and (for action cards) an `event_status` field reflecting the current `events.confirmation_status` at fetch time.

| `message_type` | Render |
|----------------|--------|
| `text` | Standard sent/received bubble (unchanged) |
| `system` | Centered, italic, muted-grey line (e.g. system events, status transitions) |
| `action` | Purple-bordered card with header "📋 Confirmation card sent" and a **live status pill** rendered by `renderEventStatusBadge(msg.event_status)` — colors match `eventConfirmationLabel`: draft (gray), pending (orange), confirmed (green), rejected/expired (red) |

Helpers added in the same file:

- `renderEventStatusBadge(status)` — returns HTML for the colored pill.
- `renderChatMessage(msg)` — switch on `msg.message_type` and delegates.
- `escapeHtml(str)` — sanitizes message text and metadata before injection.
- "Start Chat" AJAX success handler now also `$("#createEventBtn").removeClass("hide")` so the **Create Event** button appears immediately after chat is initiated, without a page refresh.

---

## Upstream Impact

- **`client_rm_requests`** — written by the public Client app (OotboAPI endpoint).
- **`admins.country_code`** — must be set on the logged-in RM, else they see zero rows.
- **`categories` master data** — joined for category name.
- **`users` table** — joined for client name + phone + email.

---

## Downstream Impact

- **Feature 09 (RM Create Event)** — the "Create Event" CTA on the detail page is the entry point.
- **`rm_conversation`** rows are read here but written by the public app / chat service.

---

## Impact of Changes

| If you change... | Risk to... | Level | Type |
|-----------------|------------|-------|------|
| Removing `client_rm_requests.country_id` | Country scoping fails → silent empty list | Critical | Data |
| Changing `record_type` / `record_id` semantics | Detail page renders wrong tab; Feature 09 storeEvent updates the wrong column | Critical | Data |
| Renumbering `rmStatus` ints | Legacy rows display "Unknown"; rows with an event are unaffected (composer routes via `eventConfirmationLabel`) | High | UI/Data |
| Renaming `events.confirmation_status` or its enum values | `eventConfirmationLabel` falls back to "—" silently and the chat panel's action-card pill goes blank | Critical | Data |
| Bypassing `rmDashboardLabel` and calling `rmStatus` directly in a view | Re-introduces the Path C seam — event lifecycle no longer reflected in the dashboard | High | UI |
| Changing `client_rm_requests.uuid` schema | Detail page can't find rows | Critical | Data |
| Adding a new status value (e.g. 6) | Falls into "Unknown" until `rmStatus()` helper is extended | Medium | UI |
| `categoryDetails` relation on a renamed table | List query throws | High | Data |

---

## Known Issues

- **Admin `country_code` is mandatory but not enforced at admin creation time.** A new RM with no `country_code` sees a completely empty list with no UI hint. Set it manually or ask superadmin.
- **`record_type` is open-typed** — code only checks for `'event'`. Any other value (`'direct'`, `''`, null) gets bucketed as "Direct Invited".
- **`getRmData` `$filter` JSON is parsed but never used** — declared then ignored. The list cannot be filtered from the UI.
- **Pagination doesn't preserve search** — DataTable's search box is non-functional server-side.
- **`payment_status` of `'1'`** displays as "Pending" but there's no UI to advance it. Payment lifecycle is owned externally (gateway webhooks via OotboAPI).
