# Story 6.1: Predictions Review Queue

## Story

**As a** data steward,
**I want** a review queue that shows only the predictions requiring my attention, with confidence scores and top evidence signals visible per row,
**So that** I can scan and act on items efficiently without opening individual detail views.

## Status

done

## Acceptance Criteria

All ACs met.

## Tasks / Subtasks

- [x] **Task 1: `PredictionService`** — New service at `src/Services/PredictionService.php`.
  - `validStatuses(): array` — returns the list of accepted status filter values.
  - `getNeedsReviewCount(): int` (static) — used by sidebar badge; wraps a COUNT query in a try/catch.
  - `getStatusCounts(): array` — COUNT(*) GROUP BY status for all tab badges; includes 'all' total.
  - `getPredictions(status, page, pageSize, confidenceLevel): array` — paginated predictions JOIN products; `needs_review` ordered confidence ASC, others created_at DESC; calls `attachTopEvidence()` for top-2 evidence source_labels.
  - `attachTopEvidence(pdo, rows): array` — single IN query for all prediction IDs on the page; groups by prediction_id and keeps top 2 by weight DESC; attaches as `top_evidence` array on each row.

- [x] **Task 2: `PredictionController`** — Updated at `src/Controllers/PredictionController.php`.
  - Constructor now accepts `PredictionService $predictionService` in addition to `EvidenceService`.
  - `index()` — reads `?status`, `?page`, `?confidence` from GET; fetches status counts + paginated predictions; reads `confidence_threshold` from `system_settings`; renders `src/Views/predictions/index.php`.
  - `listApi()` — JSON endpoint (`GET /api/predictions`); same inputs; returns `{ success, data: { predictions, total, page, total_pages, status, status_counts } }`.
  - `sanitizeStatus()` and `sanitizeConfidenceLevel()` — validate against known values with safe fallback defaults.
  - `evidenceApi()` — unchanged from prior story.

- [x] **Task 3: `src/Views/predictions/index.php`** — New view in new `predictions/` directory.
  - Queue counter ("X items awaiting review") shown only on `needs_review` tab.
  - 7 status filter tabs with count badges; active tab styled with `bg-primary` badge.
  - Text search input: client-side, filters visible rows by SKU/MPN/product name via `data-*` attributes.
  - Confidence level `<select>` filter: options labeled with threshold value; triggers AJAX reload.
  - Table: Bootstrap `.table-sm .table-hover`; columns: checkbox, SKU (monospace), MPN (monospace), Product Name, Suggested Category (label + code), Confidence Badge, Top Evidence (top 2 source_labels), Status Badge.
  - Empty state panel shown server-side when `needs_review` is active and `$predictions === []`.
  - Floating action bar (`#floating-action-bar`): fixed at viewport bottom, left-offset by 220px for sidebar; hidden until rows are checked; populated by Story 6.3.
  - AJAX tab-switching: calls `/api/predictions`, rebuilds container HTML, updates URL via `history.pushState`.
  - `window.PredictionsQueue` helper object exposes `removeRow()`, `updateRowStatus()`, `getSelectedIds()`, `getCurrentStatus()` for Stories 6.2, 6.3, 6.4.

- [x] **Task 4: `src/Views/layout.php`** — Updated to show `needs_review` count badge on Predictions nav link.
  - Calls `PredictionService::getNeedsReviewCount()` (static, guarded by `getCurrentUser() !== null` and wrapped in a try/catch in the service itself).
  - Badge hidden when count is 0.
  - Nav link updated to flex layout so badge sits on the right.

- [x] **Task 5: Routes** — Updated in `public/index.php`.
  - `GET /predictions` → `$prediction->index()`.
  - `GET /api/predictions` → `$prediction->listApi()` (auth-guarded inline).
  - `PredictionService` injected into `PredictionController` constructor.

### Review Findings

- [x] [Review][Patch] `getStatusCounts()` missing try/catch — fixed: wrapped in try/catch returning zeroed counts on DB error [`src/Services/PredictionService.php`]
- [x] [Review][Patch] `updateRowStatus()` targets `td:last-child` (Actions column) — fixed: changed to `td:nth-last-child(2)` (Status column) [`src/Views/predictions/index.php`]
- [x] [Review][Patch] Server-rendered pagination links not AJAX — fixed: added `page-link-ajax` class and `data-page` attribute, changed `href` to `#` [`src/Views/predictions/index.php`]
- [x] [Review][Defer] `submitToWorkflow()` no transaction wrapping INSERT + two UPDATEs — partial failure leaves orphaned `akeneo_workflow` row with no matching status update; `AuditLogService::write()` called outside implicit transaction scope [`src/Services/PredictionService.php:296-334`] — deferred, 6.3 scope; will be addressed in 6.3 code review
- [x] [Review][Defer] `getAllNeedsReviewIds()` no upper bound — "select all queue" can return thousands of IDs, loop per-ID with N Akeneo API calls, and exhaust PHP execution time mid-loop leaving partial state [`src/Services/PredictionService.php:359-364`] — deferred, 6.3 scope
- [x] [Review][Defer] `getPredictions()` `$pageSize` has no upper bound — caller can force full-table JOIN + IN query for top evidence in one request [`src/Services/PredictionService.php:86`] — deferred, low risk (internal callers only)
- [x] [Review][Defer] `attachTopEvidence()` top-2 selection relies on SQL ORDER BY being per-group — `ORDER BY prediction_id, weight DESC` does not guarantee per-group ordering under all query plans; PHP picks first-two rows encountered [`src/Services/PredictionService.php:193`] — deferred, edge case
- [x] [Review][Defer] `loadPredictions()` clears search input on every tab switch — user's typed search is silently wiped before fetch fires [`src/Views/predictions/index.php:354`] — deferred, UX decision
- [x] [Review][Defer] Duplicate `updateActionBar` + `clearBtn` click listener from 6.1 and 6.3 IIFEs — both fire on every checkbox change; logic is compatible but redundant; risk of divergence if either is modified [`src/Views/predictions/index.php`] — deferred, cosmetic
- [x] [Review][Defer] `evidenceCache` in 6.2 IIFE not cleared when `loadPredictions()` replaces `container.innerHTML` — stale evidence panel HTML served from cache after a tab switch [`src/Views/predictions/index.php:603`] — deferred, 6.2 scope

## Dev Notes

### Evidence signals

Top 2 evidence `source_label` values are shown inline per row. Full evidence expansion (all fields, weight percentages, missing attributes, Akeneo workflow status) is Story 6.2 scope.

### Floating action bar

The bar renders in HTML with Story 6.3 placeholders (buttons present but not wired). Story 6.3 will add CSRF-protected bulk submit logic via `window.PredictionsQueue.getSelectedIds()`.

### Empty state

A minimal empty state panel is shown for the `needs_review` tab when the queue is empty. Story 6.5 enhances it with session-activity counters (auto-approved, submitted, adjusted).

### `window.PredictionsQueue` contract

Exposed as a global helper so subsequent stories can manipulate the queue without re-implementing the counter/row logic:
- `removeRow(id)` — removes the row, decrements counter, transitions to empty state if needed.
- `updateRowStatus(id, status)` — updates the status badge cell in-place (for 6.3 submit response).
- `getSelectedIds()` — returns checked prediction IDs as int[].
- `getCurrentStatus()` — returns the active tab's status string.

### File list

**New files:** `src/Services/PredictionService.php`, `src/Views/predictions/index.php`

**Modified files:** `src/Controllers/PredictionController.php` (added index, listApi; updated constructor), `src/Views/layout.php` (sidebar badge), `public/index.php` (routes, PredictionService injection)
