# 18 — User Reports

## Overview

A read-only inbox of **abuse / quality reports** raised by clients against vendors (or vice versa) in the public apps. Reports are written by OotboAPI when end users use the in-app "Report" button; VisoAdmin surfaces them for the customer-care team.

**Status:** Live

There is **no action taken** from VisoAdmin — viewing only. Vendor suspension / refund / contact must be done manually by ops.

---

## User Stories

| ID | As a | I want to | So that |
|----|------|-----------|---------|
| RPT-01 | Customer Care (perm 4) | See a paginated list of user reports | Triage |
| RPT-02 | Customer Care | See the event ID, client phone, vendor phone, and date per report | Know who's involved |
| RPT-03 | Customer Care | Click "View" to read the full report message | Investigate |

---

## Screens & Flows

```
┌────────────────────────────────┐
│ /reports     report/list.blade │
└────────────┬───────────────────┘
             │ DataTable AJAX
             ▼
┌────────────────────────────────┐
│ /ajaxReportList                │
│ ReportsModel::ajaxList()       │
│  joins users (client + vendor) │
│  + events                      │
└────────────┬───────────────────┘
             │ click "View"
             ▼
┌────────────────────────────────┐
│ /report/{id}                   │
│  returns { description }       │
│  → rendered in modal           │
└────────────────────────────────┘
```

### Routes & Actions

| Route | Method | Handler | Description |
|-------|--------|---------|-------------|
| `/reports` | GET | `ReportController::reports()` | List page |
| `/ajaxReportList` | GET | `ReportController::ajaxReportList()` | DataTable JSON |
| `/report/{id}` | GET | `ReportController::getReport()` | Single-row JSON (just the message) |

---

## Data Model

### `user_reports`

```php
// App\Models\ReportsModel
protected $table = 'user_reports';

{
  id:         int,
  uuid:       string,
  event_id:   string|null,         // added by migration 2025_02_25 — VARCHAR(50)
  client_id:  int,                 // FK users.id
  vendor_id:  int,                 // FK users.id
  message:    string,              // the report body
  created_at, updated_at,
}
```

> The `event_id` was added later as `VARCHAR(50)` — so it may store UUID strings rather than ints. The `ReportsModel::ajaxList` joins `user_reports.event_id = events.id` which is an integer column. **Verify**: if `event_id` is UUID, the join silently misses rows; if integer-string, it works.

### Joined Display Columns

```
human_usable_id (from events)
event_name      (from events)
client_name     = client.display_name
client_phone    = client.phone
vendor_phone    = vendor.phone
created_at
```

---

## Validations & Business Rules

| Rule | Detail |
|------|--------|
| No mutation surface | Read-only |
| Sort | `created_at DESC` |
| Pagination | DataTable `start, length` |
| `client_id` and `vendor_id` are required for the inner joins | Reports with null vendor_id or client_id would be excluded from the list |
| Deleting a vendor (Feature 05) | Cascades to `user_reports WHERE vendor_id = N` |

---

## API Endpoints

| Method | Path | Auth | Request | Response | Consumer |
|--------|------|------|---------|----------|----------|
| GET | `/reports` | session + permission 4 | — | HTML | Browser |
| GET | `/ajaxReportList` | session (AJAX whitelisted) | `draw, start, length` | DataTable JSON | List page |
| GET | `/report/{id}` | session + permission 4 | path id | `{ description: <message> }` | Modal |

---

## Upstream Impact

- **`user_reports` table** — written by OotboAPI when end users tap "Report".
- **`users` (clients + vendors)** — joined for phone display.
- **`events`** — joined for event_name + human_usable_id display.

---

## Downstream Impact

- **No automated downstream** — humans take action manually.

---

## Impact of Changes

| If you change... | Risk to... | Level | Type |
|-----------------|------------|-------|------|
| `user_reports.event_id` type drift (int ↔ uuid) | Join silently drops all matching reports | High | Data |
| Renaming `user_reports.message` | Modal returns null | High | Data |
| Removing the inner-join (e.g. allow null vendor) | Reports with null vendor disappear | Medium | Data |
| Adding required columns | OotboAPI insert must align — independent of this UI | Low | Data |

---

## Known Issues

- **No action surface.** No "Mark resolved" / "Suspend vendor" / "Reply to client" button. Pure read.
- **`event_id` type ambiguity** — see Data Model.
- **Inner joins drop reports** with null `client_id` or `vendor_id`. Pre-event reports (client reports vendor before booking) could be excluded if data isn't shaped right.
- **No filter by date range or vendor** — large report volume requires scrolling.
