# Agent Console V2 Plan

A full revamp of [pages/ticket/agent_console.php](../pages/ticket/agent_console.php) and its backend [backend/ticketGetLists.php](../backend/ticketGetLists.php). The current console is slow, polls too much, renders too much server-side, and centers on metrics (aging, time-open) that no longer reflect how we measure work.

## Why the current console is slow

1. **N+1 sessions queries.** For every visible ticket in `User`, `Reminders`, `Active`, `ClosedAgent`, and `Closed`, the backend issues a separate `get_items("ticket_sessions", …)` and then runs `flatten_session_time` in PHP. With ~hundreds of active/closed tickets per poll cycle this is the dominant cost. See [ticketGetLists.php:277](../backend/ticketGetLists.php), [:384](../backend/ticketGetLists.php), [:570](../backend/ticketGetLists.php), [:630](../backend/ticketGetLists.php), [:695](../backend/ticketGetLists.php).
2. **Every poll runs every view.** Even though only one tab is visible, the backend builds all 7 tab payloads (stats, user, reminders, unassigned, unassigned-agent, active, closed, closed-agent, unbound-calls) on every 20s tick. `view=` exists but the page never uses it.
3. **Server-rendered HTML in every response.** Each poll ships full `<tr>` HTML for every list, and the client `.html()`-swaps tbodies, blowing away tablesorter state and forcing a re-update.
4. **Stats does 18 separate `COUNT(*)` round-trips.** `OpenToday/ClosedToday/.../CallDurationMonthLast`. Each is a discrete query.
5. **Topic list rebuilt on every page load.** `get_ticket_topics()` plus the grouping/sorting dance ([agent_console.php:30-51](../pages/ticket/agent_console.php)) runs on every render.
6. **Tablesorter rebinds on every refresh.** `trigger("update")` on 7 tables, 3× per minute.

## What's outdated

- **Aging.** `calc_aging_at` and the 2-day "aging" warning are no longer how we judge workload. Move to **session-based activity**: "last session at", "time logged today / this week / on this ticket".
- **Time Open.** Wall-clock `closed_at - created_at` is meaningless — a ticket held for parts on order looks identical to one that ate 20 hours of work. Replace with **Time Logged** (sum of sessions) everywhere.
- **Ticket open/close as a workload signal.** Open/close counts stay only as historical totals on stats; they don't drive any of the agent's day-to-day views.
- **Hardcoded user IDs.** `in_array($_SESSION['user']['pk_user'], [1, 90])` gates Stats and Unbound Calls. Replace with a permission (e.g. `Connect.HelpDeskAgent.AdvancedConsole` or equivalent).
- **Hidden manager-impersonation form.** The `if (0 && $_SESSION['user']['pk_user'] == 1)` block ([agent_console.php:102](../pages/ticket/agent_console.php)) is dead code; the `?agent=` impersonation path stays but is exposed cleanly.

## V2 design

### Information architecture

One page, two primary sections (My Work + All Tickets), in this order:

1. **Header strip** — system selector, "Create Ticket", and (for managers / when `?agent=` is set) the agent picker. Inline, not buried in dead conditionals.
2. **My Work** — a focused view of what this agent should be doing right now. Three sub-tabs:
   - **Active** — open tickets assigned to me. Default sort: most recent session activity first.
   - **Reminders** — open tickets I own with a reminder set. Overdue first, badge with count. List view only (no calendar/timeline in V2).
   - **Unclaimed (mine)** — unclaimed tickets in topics I cover. Red badge when non-zero.
3. **All Tickets** — a unified search/browse view that replaces today's `Unassigned`, `All Active`, `All Closed`, and the existing closed-filter row.

`My Stats` moves to a secondary nav (or a small expand-section) so it doesn't compete with the work surface. **Unbound Calls is removed** from the agent console entirely — the manager console has a better version.

### Unified search ("All Tickets")

One filter bar drives a single list. Filters:

- **Free text** — matches `message_brief`, `message_status`, `pk_ticket`.
- **Location** — autocomplete on entities (reuse `do/searchLocations`).
- **Topic** — multi-select grouped by system (reuse existing topic grouping).
- **Date range** — single picker with presets (Today, This Week, Last 2 Weeks, This Month, Custom). Applies to `created_at` or `closed_at` per a "Match by" toggle (opened / closed / updated).
- **Status** — multi-select including a synthetic "Open" / "Closed" / "On Hold".
- **Agent** — multi-select, defaults to all.
- **Severity** — chips.

The same list also supports a "scope" toggle: **Mine**, **My topics**, **All**.

Filters serialize into the URL hash so a search is shareable / bookmarkable.

### Per-row data (the columns that survive)

| Column | Notes |
|---|---|
| Platform | unchanged |
| Topic + #id + brief | unchanged |
| Status | unchanged |
| Location | unchanged |
| Contact | unchanged |
| Agent | shown in All Tickets and agent-impersonation mode |
| **Last Session** | timestamp of most recent session end (or "in session" pill if active) — replaces "Last Update" as the primary recency cue |
| **Time Logged** | cumulative session time on the ticket; for current week, hover shows today / this week / total |
| Reminder | only on Reminders tab |

Gone: "Aging", "Time Open", `calc_aging_at`-based highlighting. Severity-based row tinting stays.

### Agent deep-dive (when `?agent=` is set, or you click your own name)

A dedicated route (`/ticket/agent/<pk_user>`) instead of just swapping the title in the same template:

- Top: name, role, today's session time, this week's session time, current in-session ticket.
- **Session activity sparkline** — sessions/time logged per day for last 4 weeks (Chart.js).
- **Workload** — open tickets bucketed by status, by topic, by age-of-last-session.
- **Recent sessions** — last 25 sessions with ticket, duration, contact type, comment.
- **Reminders** and **Unclaimed-in-topics** lists, same shape as the main console.

Linked from the manager status page and from clicking an agent name anywhere in the all-tickets list.

### Stats tab (kept, simplified)

The 4×3 "Open / Today / This Week / Last Week" header block and the second time-span table collapse into **one** table keyed by time span (Today, This Week, Last Week, This Month, Last Month) with columns: *Opened, Closed, Sessions, Time Logged, Calls, Call Duration*. This is what `htmlNormal.Stats` already does at the bottom — promote it and drop the duplicate header. Gate on permission, not hardcoded ID.

### Unbound Calls — removed

Dropped from the agent console entirely. The manager console has a better version; agents who need it go there. Delete the `UnboundCalls` view from the backend, the tab from the page, and the call-related modals/handlers (`btnCallAssignTicket`, `btnCallDismissTicket`, `btnCallTagTriage`, `formTicketTagTriage`, `modalTicketTagTriage`) — unless any are reused elsewhere; grep before deleting.

## Backend changes

### Cached flattened session time on the ticket row

Add two columns to `tickets`:

- `cached_session_seconds INT NOT NULL DEFAULT 0` — total flattened (de-overlapped) session time across all agents.
- `last_session_at DATETIME NULL` — `MAX(end_dt)` from non-deleted sessions.

Maintained on every session insert / update / delete / soft-delete by re-running the existing `flatten_session_time` for that ticket and writing the result back. The hot paths to instrument:

- [backend/ticketSet.php](../backend/ticketSet.php) (session save / status transitions)
- Wherever sessions end / are deleted (`do/sessionEnd` etc. — grep `ticket_sessions` writes)
- A one-shot backfill script in [scripts/](../scripts) to populate existing rows.

Every list view then reads `cached_session_seconds` and `last_session_at` as plain columns — no join, no PHP flattening, no N+1. This is what makes the 20s poll cheap.

For agent-specific time logged (e.g. "Time Logged by *this* agent on this ticket"), we still need a per-agent flatten. Two options, decide at implementation time:
- Compute on demand for visible rows only (cheap because we limit the page).
- Add a `ticket_session_totals` summary table keyed `(fk_ticket, fk_user)` updated on the same hooks.

Lean toward on-demand; promote to a summary table only if profiling shows it matters.

### Endpoints

Split the monolithic `ticketGetLists` into:

- `do/ticketConsoleCounts` — returns badge counts + a single hash for change detection. Cheap. Polls every 15-20s.
- `do/ticketConsoleList` — returns JSON rows for **one** view (`view=active|reminders|unclaimedMine|all|closedMine|...`) with the current filters. Called on tab open and when filters change. Not on a timer.
- `do/ticketConsoleStats` — stats payload. Called once on tab open, refreshed on manual reload.

Counts endpoint targets <100ms; the others can be slower because they only fire on user action.

### JSON, not HTML

Endpoints return rows as JSON. Rendering moves to the client (small template, ~50 lines of jQuery or vanilla JS, no framework needed — matches existing stack). Benefits:

- Response size drops sharply.
- Client diffs rows on update (keyed by `pk_ticket`) instead of `.html()`-blowing away tablesorter state.
- Filtering and sorting can happen client-side for small result sets, falling back to the server for large ones.

### Polling

- Counts endpoint: every 20s on `appMovedToForeground` and a `setInterval`. Same cadence as today, but the payload is ~1KB instead of dozens of KB.
- Active list: re-fetch when its count hash changes, or every 60s, whichever comes first. Not every 20s.
- Other lists: only on tab open or filter change.

### Cache friendliness

`get_ticket_topics`, `get_ticket_statuses`, and the agents list rarely change. Cache to `$_SESSION` with a short TTL (or the existing site cache layer) so the page render does almost no DB work.

## Migration / rollout

1. **Phase 1 — backend foundation (no UI change yet)**
   - Add `tickets.cached_session_seconds` and `tickets.last_session_at` columns.
   - Backfill via a one-shot script under [scripts/](../scripts).
   - Update session-write paths to maintain them.
   - Add the split endpoints alongside the existing `ticketGetLists`.
2. **Phase 2 — V2 page at a new route**
   - Build `pages/ticket/agent_console_v2.php` and link it from a "Try the new console" banner on the old page. Old page stays live.
   - Iterate based on agent feedback (us).
3. **Phase 3 — agent deep-dive page** at `/ticket/agent/<pk_user>`.
4. **Phase 4 — cutover**
   - Swap the `ticketConsole` route to V2.
   - Delete the old [pages/ticket/agent_console.php](../pages/ticket/agent_console.php) and [backend/ticketGetLists.php](../backend/ticketGetLists.php) once nothing else points at them. Grep first — `updateAging.php` and `ticketManagerGetOverview.php` also reference `calc_aging_at`; decide whether to drop the column or leave it.

## Decisions locked in

- **Session time:** cached flattened column on `tickets`. No simple-SUM fallback in list views.
- **Layout:** two sections — My Work (Active / Reminders / Unclaimed-mine) and All Tickets.
- **Reminders:** list only, no calendar/timeline.
- **Unbound Calls:** removed from the agent console; the manager console version stays as the canonical one.

## Open questions

- Per-agent time-logged on a ticket: compute on demand for visible rows, or add a `(fk_ticket, fk_user)` summary table? Pick at implementation time based on profiling.
- Permission name for the Stats tab gating — reuse an existing perm or add a new one (e.g. `Connect.HelpDeskAgent.Stats`)?
- Whether `calc_aging_at` can be dropped from the schema once V2 is live — [updateAging.php](../backend/updateAging.php) and [ticketManagerGetOverview.php](../backend/ticketManagerGetOverview.php) still use it on the manager side. Probably leave the column, stop showing it in the console.
