# 10 — Dashboard / Critical Events

## Overview

The main dashboard for full superadmins (`superadmin = 1`). Displays a paginated list of **critical events**: events with **at least one service that has zero quotes** AND `event_date >= today`. These are the events most likely to slip if ops don't intervene.

**Status:** Live

---

## User Stories

| ID | As a | I want to | So that |
|----|------|-----------|---------|
| DSH-01 | Superadmin | Land on `/dashboard` after login | I have a single home screen |
| DSH-02 | Superadmin | See a table of upcoming events with no vendor quotes | I can prioritise outreach |
| DSH-03 | Superadmin | See per-event quoted vs non-quoted service counts | I know how close to fulfilled an event is |
| DSH-04 | Superadmin | Click into an event's detail | I can take action |
| DSH-05 | Superadmin | See the dashboard chrome (sidebar, notification badge) | I have one-click navigation to any feature |

---

## Screens & Flows

```
       Login (superadmin=1) ──▶ /welcome ──▶ /dashboard
                                                  │
                                                  ▼
┌──────────────────────────────────────────────────────────┐
│ /dashboard       dashboard/index.blade.php               │
│                                                          │
│  ┌──────────────────────────────────────────────────┐    │
│  │ DataTable: human_id | event_name | phone |       │    │
│  │  date | time | city | state | status |           │    │
│  │  quoted | non-quoted | actions                   │    │
│  └──────────────────────────────────────────────────┘    │
└──────────────────────────┬───────────────────────────────┘
                           │ DataTable AJAX
                           ▼
                ┌────────────────────────────────────────┐
                │ /event/ajaxCriticalEvents              │
                │ HomeController::ajaxCriticalEvents()   │
                │ → EventModel::criticalEvents()         │
                │   + EventService::getEventData()       │
                └────────────────────────────────────────┘
```

### Routes & Actions

| Route | Method | Handler | Description |
|-------|--------|---------|-------------|
| `/dashboard` | GET | `HomeController::index()` | Render dashboard view |
| `/event/ajaxCriticalEvents` | GET | `HomeController::ajaxCriticalEvents()` | DataTable JSON |

---

## Data Model

Reads from:
- `events` — fields displayed
- `event_services` — to determine "has at least one service with zero quotes"
- `event_services_vendor_quote` — the absence-of joins
- `users` (clients) — for client phone
- `cities` (location)
- `states`

### The Critical-Events SQL (simplified)

```sql
SELECT events.human_usable_id,
       events.id AS event_id,
       events.event_name,
       events.event_date,
       events.event_start_time,
       events.is_cancelled,
       (SELECT COUNT(id) FROM event_services WHERE event_id = events.id) AS total_services,
       u.display_name AS clientname,
       (SELECT name FROM cities WHERE id = events.cities_id) AS location,
       (SELECT name FROM states WHERE id = events.state_id) AS state_name,
       u.phone
FROM events
JOIN users u ON events.client_id = u.id
WHERE events.id IN (
  SELECT GROUP_CONCAT(DISTINCT event_id) AS events
  FROM event_services es
  LEFT JOIN event_services_vendor_quote eq ON es.id = eq.event_services_id
  WHERE eq.id IS NULL
  GROUP BY es.event_id
)
AND DATE(events.event_date) >= CURDATE()
ORDER BY events.event_date DESC
LIMIT <start>, <length>
```

### Per-Row Computed (via `EventService::getEventData`)

```php
quoted:     COUNT(service_id WHERE quote_count > 0)
non_quoted: COUNT(service_id WHERE quote_count = 0)
```

---

## Validations & Business Rules

| Rule | Detail |
|------|--------|
| Only future events | `DATE(events.event_date) >= CURDATE()` |
| Only events with at least one un-quoted service | NULL-LEFT-JOIN trick |
| Pagination | DataTable `start`, `length` interpolated into LIMIT clause |
| No filter for `is_cancelled` | Cancelled events ARE included if event_date is future — see Known Issues |
| Sort | `ORDER BY event_date DESC` |
| Per-row "Details" links to `/event/details/{id}` | Reuses Feature 06 |

---

## API Endpoints

| Method | Path | Auth | Request | Response | Consumer |
|--------|------|------|---------|----------|----------|
| GET | `/dashboard` | session + permission 2 | — | HTML | Browser |
| GET | `/event/ajaxCriticalEvents` | session (AJAX whitelisted) | `draw, start, length` | DataTable JSON | Dashboard DataTable |

---

## Upstream Impact

- **`events`, `event_services`, `event_services_vendor_quote`** — populated by OotboAPI (and `events` also by Feature 09).
- **`users`** — for client name + phone column.
- **`cities`, `states`** — for location columns.
- **`EventService::getEventData`** helper — used here and in detail views.

---

## Downstream Impact

- **Feature 06 (Event Oversight)** — "Details" link points there.
- **Operations behaviour** — this list is the daily morning briefing for ops. Changes to its semantics ripple to "what we follow up on".

---

## Impact of Changes

| If you change... | Risk to... | Level | Type |
|-----------------|------------|-------|------|
| Adding `is_cancelled = 0` to the WHERE clause | Cancelled future events disappear (might be desired but currently they show) | Medium | Data |
| Renaming `event_services_vendor_quote.event_services_id` | The NULL-LEFT-JOIN breaks — every event becomes critical | Critical | Data |
| Changing `events.event_date` type to datetime | `DATE(event_date) >= CURDATE()` still works but timezone semantics shift | Medium | Data |
| Removing `cities.name` or `states.name` columns | Two subqueries return null → location/state columns blank | High | Data |
| Reordering `LIMIT $start, $length` to use sql placeholders | If you switch to bind params you must rewrite the trailing concat | Medium | Service |

---

## Known Issues

- **`LIMIT $start, $length` is string-interpolated** — values come from `$request->start` and `$request->length`. Laravel's request cast keeps them numeric, but a malformed POST body could inject SQL. Replace with bound params eventually.
- **Cancelled events with future dates ARE listed** — there is no `is_cancelled = 0` filter. Ops may waste time chasing them.
- **N+1 inside the loop** — `EventService::getEventData($row->event_id)` runs once per row, each issuing an extra query. 10 rows = 11 queries.
- **Display columns include `event_start_time`** formatted by `viso_time_format` (`h A` — hour only, no minutes). Ops may misread "2 PM" for an event starting "2:30 PM".
- **The list excludes events with NO services at all** because the inner subquery joins `event_services`. An event with zero services would not appear — but this state is unreachable through normal flows (Feature 09 enforces services).
