# 07 — Coordinator Console

## Overview

A dedicated workbench for **Coordinators** (`admins.superadmin = 2` OR permission group 7). Coordinators help bridge clients and vendors for individual events — they look up an event by its human ID, see the quote landscape, see candidate vendors per service+city, and read chat threads.

**Status:** Live

> Coordinator UX largely mirrors Feature 06 (Event Oversight) but has its own controller + views because:
> 1. Coordinators land on `/coordinator` (not `/event/list`) right after login.
> 2. They look up by human ID (`EVN0000123`) instead of internal numeric `events.id`.
> 3. They have additional surfaces: vendor-finder dropdown, vendor mini-card details.

---

## User Stories

| ID | As a | I want to | So that |
|----|------|-----------|---------|
| COR-01 | Coordinator | Land on `/coordinator` after login | I'm in my workbench immediately |
| COR-02 | Coordinator | Enter an event's human-readable ID and open its details | I can help clients reference their bookings |
| COR-03 | Coordinator | See all vendor quotes per service for that event | I know who's bidding |
| COR-04 | Coordinator | Get a list of vendors offering a service in the event's city WHO HAVEN'T quoted yet | I can manually invite them |
| COR-05 | Coordinator | Pop open a quick vendor mini-card | I can ring them |
| COR-06 | Coordinator | Read the full chat thread between client and a specific vendor | I can de-escalate disputes |

---

## Screens & Flows

```
       Login (superadmin=2) ──▶ WelcomeController ──▶ /coordinator
                                                          │
                                                          ▼
┌───────────────────────────────────────┐
│ /coordinator                          │
│ index.blade.php                       │
│ ┌──────────────────────────────────┐  │
│ │ Enter human_usable_id (EVN...)   │  │ ──▶ form GET /coordinator/{id}
│ └──────────────────────────────────┘  │
└───────────────────┬───────────────────┘
                    │ valid id
                    ▼
┌──────────────────────────────────────────────────────────────────┐
│ /coordinator/{human_id}    coordinator/details.blade.php         │
│  ┌─────────────┐ ┌─────────────────┐ ┌─────────────────────┐     │
│  │ Event card  │ │ Services        │ │ Vendor quotes       │     │
│  └─────────────┘ └─────────────────┘ └──────────┬──────────┘     │
│                                                 │                │
│                                                 ▼                │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │ "Find Vendors" per (service, city) → /coordinator/vendors/  │ │
│  │   {service}/{location}/{eventid}                            │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                                                                  │
│  Vendor click ──▶ /coordinator/vendorDetails/{id}  (JSON modal)  │
│  Chat tab ──▶ /coordinator/event-chat   (reuses event chat)      │
└──────────────────────────────────────────────────────────────────┘
```

### Routes & Actions

| Route | Method | Handler | Description |
|-------|--------|---------|-------------|
| `/coordinator` | GET | `CoordinatorController::index()` | Home — search box |
| `/coordinator/{id}` | GET | `CoordinatorController::details()` | Detail page, `$id` = `human_usable_id` |
| `/coordinator/vendorDetails/{id}` | GET | `CoordinatorController::vendorDetails()` | Mini-card JSON |
| `/coordinator/vendors/{service}/{location}/{eventid}` | GET | `CoordinatorController::vendorList()` | Candidate-vendor JSON |
| `/coordinator/event-chat` | GET | `EventController::eventChats()` (shared) | Chat thread JSON |
| `/coordinator/event-services` | GET | `EventController::eventServices()` (shared) | Service row JSON |

### Middleware Gating

- Either `superadmin == 2` OR permission group 7. See `UserPermission::handle()` — it has an explicit branch `(auth()->user()->superadmin == 2 && in_array($currentRequest, $coordinatorPermission))`.
- The `permissionGroup()` helper returns `7` whenever the URI contains the substring `coordinator` (special-case in the helper).

---

## Data Model

Reads from the same tables as Feature 06:

- `events` (lookup is by `human_usable_id`, NOT `id`)
- `event_services`
- `event_services_vendor_quote`
- `event_vendor_discount`
- `users` (vendor names)
- `services`
- `cities`
- `vendor_services` + `vendor_services_locations` (for the "available vendors" query)
- `conversation` + `chat_service` + `chat` (for chat & is-conversation-initiated checks)

Sample (vendor candidate query, from `VendorUser::locationVendors`):

```sql
SELECT users.display_name as name, users.phone
FROM vendor_services vs
JOIN vendor_services_locations vsl ON vs.id = vsl.vendor_services_id
JOIN users ON vs.vendor_id = users.id
WHERE vs.service_id    = :service_id
  AND vsl.location_id  = :city_id
  AND users.id NOT IN (
    SELECT quote.vendor_id
    FROM event_services_vendor_quote quote
    JOIN event_services es ON quote.event_services_id = es.id
    WHERE es.service_id = :service_id
      AND es.event_id   = :event_id
  )
```

---

## Validations & Business Rules

| Rule | Detail |
|------|--------|
| Detail lookup by human id | If `human_usable_id` doesn't match, flash + redirect to `/coordinator` |
| Vendor candidate list excludes vendors who already quoted | See SQL above |
| Same per-vendor quote aggregation as Feature 06 | Uses `EventModel::getEventVendorQuotesDetails($eventDetails->id)` |
| Conversation id resolved per vendor | `EventModel::getConversationDetails($vendor_id, $client_id, $eventDetails->id)` |
| Reads `is_hired` and `chat_initiated` per vendor | For badge rendering |

---

## API Endpoints

| Method | Path | Auth | Request | Response | Consumer |
|--------|------|------|---------|----------|----------|
| GET | `/coordinator` | session + perm 7 OR superadmin∈{1,2} | — | HTML | Browser |
| GET | `/coordinator/{human_id}` | session + perm 7 OR superadmin∈{1,2} | path human_id | HTML | Browser |
| GET | `/coordinator/vendorDetails/{vendorId}` | session + perm 7 OR superadmin∈{1,2} | path vendorId | `{ fullname, email, phone, company_name, display_name, address }` | Detail modal |
| GET | `/coordinator/vendors/{service}/{location}/{eventid}` | session + perm 7 OR superadmin∈{1,2} | path params | `[{name, phone}, ...]` OR `null` | Find-vendor dropdown |
| GET | `/coordinator/event-chat` | session + perm 7 OR superadmin∈{1,2} | `userId[], convId, eventid, client` | Chat thread (same shape as Feature 06) | Chat panel |
| GET | `/coordinator/event-services` | session + perm 7 OR superadmin∈{1,2} | `serviceId` | Service row | Edit modal |

---

## Upstream Impact

- **Feature 06 (Event Oversight)** shares model methods.
- **Feature 05 (Vendor Mgmt)** — vendor records + their service+city bindings drive the "candidates" query.
- **Feature 12 / 13 (Master Data)** — service & city ids in the URL.
- **Coordinator auth setup** — `admins.superadmin = 2` is the only way to reach this without explicit permission grant.

---

## Downstream Impact

- **Viso-Chat** — chats viewed here are persisted by Viso-Chat originally.
- **OotboAPI** — vendor quote submissions originate there; coordinator reads them.

---

## Impact of Changes

| If you change... | Risk to... | Level | Type |
|-----------------|------------|-------|------|
| Changing `events.human_usable_id` format | Coordinator search by ID fails | High | Data |
| Adding a permission tier between coordinator and superadmin | The `Coordinator` middleware's `superadmin == 2` shortcut may bypass new checks | High | Guard |
| Removing the substring-match `coordinator` rule in `permissionGroup()` | All `/coordinator/*` URIs lose perm-mapping → 403 | Critical | Guard |
| Renaming `VendorUser::locationVendors` SQL columns | Coordinator finder dropdown breaks | High | Data |
| Changing `vendor_services.service_id` or `vendor_services_locations.location_id` to UUIDs | Candidate query joins break (currently integer-based) | High | Data |

---

## Known Issues

- **N+1 query inside `details()`**: same as Feature 06 — `getEventVendorQuotesDetails($eventDetails->id)` is called inside the foreach loop.
- **`vendorList()` returns `null`** (not an empty array) when no candidates — JS consumers must handle both shapes.
- **No filter for `users.status`** in `locationVendors()` — inactive vendors are listed as candidates.
- **Chat panel and event-chat route share `EventController::eventChats`** which expects `userId[]` array; coordinator must build this array client-side. Mis-shape → empty thread.
