# Story 6.3: Bulk Submit to Akeneo Collaborative Workflow

## Story

**As a** data steward,
**I want** to select multiple predictions and submit them to Akeneo Collaborative Workflow in one action,
**So that** I can process a batch of low-confidence items without submitting each one individually.

## Status

done

## Acceptance Criteria

All ACs met.

## Tasks / Subtasks

- [x] **Task 1: `PredictionService::submitToWorkflow()`** — New method.
  - Accepts `int[] $predictionIds`, `int $userId`, `AkeneoService $akeneo`.
  - Skips predictions not in `needs_review` status; adds to `failed` with reason.
  - Skips products with no `akeneo_identifier`; adds to `failed` with reason.
  - Builds evidence summary from top 3 evidence rows (source_label: value; ...) for the Akeneo draft.
  - Calls `AkeneoService::submitToCollaborativeWorkflow()` per prediction.
  - On success: inserts `akeneo_workflow` record (status `submitted`), updates `predictions.status` and `products.status` to `submitted`, writes `audit_log` entry (`submitted_to_workflow`, actor `user`).
  - On Throwable: maps HTTP status codes to human-readable reasons (404 → "not found", 429 → "rate limit", 401 → "auth failed", 403 → "permission denied"); logs warning via Logger; adds to `failed`.
  - Returns `{ submitted: int[], failed: [{id, sku, reason}] }`.

- [x] **Task 2: `PredictionService::getAllNeedsReviewIds()`** — Returns all `needs_review` prediction IDs ordered by confidence ASC; used for "select all in queue" submit.

- [x] **Task 3: `PredictionController`** — Updated constructor to accept `AkeneoService $akeneo`.
  - `submitBulk()` — POST `/predictions/submit-to-workflow`; validates CSRF; reads `prediction_ids` (comma-separated) or `select_all_queue` flag from POST body; calls `submitToWorkflow()`; returns JSON.
  - `submitSingle(int $id)` — POST `/predictions/{id}/submit-to-workflow`; validates CSRF; calls `submitToWorkflow([$id], ...)`; returns JSON.

- [x] **Task 4: Routes** — Added to `public/index.php`:
  - `POST /predictions/submit-to-workflow` (exact match, ahead of pattern routes).
  - `POST /predictions/{id}/submit-to-workflow` (numeric ID pattern).
  - `$akeneo` injected into `PredictionController` constructor.

- [x] **Task 5: View — Actions column** — Added 9th column "Actions" to table header (PHP and JS builders).
  - Per-row "Submit" button (`btn-row-submit`, `data-pred-id`) shown only for `needs_review` rows.
  - Evidence panel colspan updated from 8 → 9.

- [x] **Task 6: View — Floating action bar** — Updated `#floating-action-bar` HTML.
  - `data-csrf` and `data-needs-review-total` attributes carry PHP values into Story 6.3 IIFE.
  - Added `#select-all-queue-notice` div: shown when all visible rows are selected and queue has more items; "X items on this page selected. Select all Y items in queue?" link sets `selectAllQueue = true`.
  - Added `#submit-result-banner` for partial failure messages.

- [x] **Task 7: View — Story 6.3 IIFE** — Third `<script>` block in `predictions/index.php`.
  - Delegated click on `.btn-row-submit`: disables button, calls `submitIds([predId], false, btn)`.
  - Action bar submit button: gathers `getSelectedIds()` or `selectAllQueue = true`; calls `submitIds(ids, allQueue, null)`.
  - `submitIds()`: POSTs form-encoded body (`_csrf_token`, `prediction_ids` or `select_all_queue=1`); on success calls `PredictionsQueue.removeRow()` for each submitted ID, resets bar, shows partial-failure banner if needed.
  - Partial failure banner: "{M} items could not be submitted. The remaining {N} were submitted successfully. [View errors]" with toggle to reveal per-SKU reasons.
  - Action bar auto-hides when no checkboxes are checked; `selectAllQueue` flag resets on any manual checkbox change or Clear.

## Dev Notes

### CSRF delivery

The submit endpoints use `application/x-www-form-urlencoded` POST (same as existing admin endpoints). CSRF token is stored in `data-csrf` on the action bar element (set from PHP `CsrfMiddleware::getToken()`), not in a global JS variable, to keep it encapsulated in the IIFE.

### "Select all in queue" flag

When `select_all_queue=1` is in the POST body, `submitBulk()` calls `getAllNeedsReviewIds()` server-side rather than trusting a client-supplied list, which avoids any risk of a user forging a larger-than-visible set.

### File list

**Modified files:** `src/Services/PredictionService.php` (submitToWorkflow, getAllNeedsReviewIds, humanizeAkeneoError added), `src/Controllers/PredictionController.php` (AkeneoService constructor arg, submitBulk, submitSingle), `src/Views/predictions/index.php` (Actions column, action bar, Story 6.3 IIFE, colspan 9), `public/index.php` (routes, updated PredictionController instantiation)
