# PRD — bi_tool_frontend_phase6

Evidence date: 2026-10-19. Source: `git@bitbucket.org:overdrive/bitool_frontend.git`, branch `phase_6` at `82d7ef04` (2026-08-21, Jason Wandrag, "Domestic reports and Firefox bugfixes").

Silicon Ark had no prior PRD for this directory. The source repo has `README.md`, `DEVELOPER_DOCUMENTATION.md`, `CHANGELOG.md`, `scope/User_Flow_Scope.md`, and `NewKnowledgeBase.md`. Where those disagree with the code, the requirement states the code and marks the document as conflicting.

Provenance labels: **Documented**, **Implemented**, **Inferred**, **Planned**, **Unknown**.

## Product purpose

**Documented** (`scope/User_Flow_Scope.md`, Home copy in `src/pages/Home.js`): SAWIS (SA Wine Industry Information and Systems) BI. Visitors read wine-industry information. The Home "Our Vision" text says the aim is online, real-time access to relevant information for the wine industry.

**Implemented:** four report areas (domestic sales, exports, grape production, vineyard status) plus an admin console for mapper imports and SAWIS 7 domestic returns.

**Unknown / Requires Confirmation:** which of these areas are in production on `phase_6` versus still on `qa` or `master`, and which audience (producers, SAWIS staff, DAFF, the public) is allowed to see each report in the business — the UI gates are below; the organisational policy is not in this repo.

## Users and roles

Roles are numbers on `user.admin_role`. Labels in `src/pages/admin/UsersListPage.jsx`:

| `admin_role` | Label in the user table | What the UI allows | Provenance |
|---|---|---|---|
| absent / not logged in | — | Home, Exports, Grape Production, Vineyard Status. No Domestic Sales, no value tables, no Admin | **Implemented** `Navbar.js`, `Home.js`, `Reports.js` |
| 0 | User | Same as a logged-in non-admin. Capture only if `domestic.permissions.can_capture` or `is_domestic_producer` | **Implemented** `domesticPermissions.js` |
| 1 | DAFF | Admin link. Tools only. Direct `/admin/users` and `/admin/submissions` redirect to tools | **Implemented** `canAccessAdminTools`, `canAccessAdminSubmissions`, `Admin.js` |
| 2 | Dev | Tools, user management, SAWIS 7 submissions. Can cancel and reopen. Can assign super users. Default admin landing is Tools | **Implemented**. Comments also call this role Developer |
| 3 | Super User | User management and SAWIS 7 submissions. No Tools. `/admin` lands on submissions. Promote is hidden while they are already role 3; Demote sets role 0 | **Implemented** `UsersListPage.jsx`, `LinkUserPanel.jsx` |
| 4 | SU Admin | Tools, user management, submissions. May type any producer code on capture with no assigned pool. Can cancel and reopen | **Implemented** `canTypeAnyProducerCode` |

Permission flags on `user.domestic.permissions`, when that object exists, override the role fallbacks for view, capture, approve, manage users, and assign super users (`permissionsFromUser`).

**Inferred gap:** `authStore.reconstructDomestic` (used when `GET auth/domestic/me` is a 404) sets `can_approve` only for roles 3 and 4. `canApproveDomestic`'s role fallback includes 2, 3, and 4. If the live `permissions` object is missing, role 2 does not get `can_approve` from the reconstructed object, and `canAccessAdminSubmissions` then follows that flag. **Requires confirmation** against the backend contract in `src/api/contracts/phase6/` (files were referenced; this PRD did not re-validate each JSON sample).

`canViewDomesticReports` is exported and unused by pages. Domestic routes use `AuthRouteGuard` (any logged-in user), not that helper.

## Main workflows

### W1 — Open the app and resume a session

Trigger: the bundle loads.

`authStore.js` calls `autoLogin()` at module scope, and `App.js` calls it again on mount.

Inputs: cookie `sawisAuth` (`path=/`, `sameSite: Lax`), else `localStorage` key `sawis_auth_v1`, else a legacy `jwtToken:::…+user:::…` cookie.

Behaviour: if token and email parse, the user is set and `GET auth/domestic/me` is attempted. 404 or 403 on `/me` is a gap: keep login `domestic` data, else `sessionStorage` key `phase6_domestic`, else rebuild permissions from role. 401 on later API calls clears the session and alerts (`authSession.js`). 403 does not.

Output: navbar shows the email local-part, or Login.

### W2 — Sign in

Trigger: navbar Login opens `LoginForm`.

Inputs: email, password, `typeOfLogin: 1`, reCAPTCHA token (or `"test-bypass"` when `REACT_APP_DISABLE_RECAPTCHA=true`).

Steps:

1. If `REACT_APP_MOCK_AUTH=true`, skip the API and return a fake user with `admin_role` `"1"`. `.env.development` sets this flag to `false`.
2. Otherwise `POST /loginAuth`. Body is plain JSON when `REACT_APP_API_BASE_URL` is empty or contains localhost / 127.0.0.1. Otherwise the body is `{ payload }` AES-256-CBC, key `REACT_APP_LOGIN_PAYLOAD_KEY` (`encryptLoginPayload.js`).
3. Header `Content-Type` is omitted on purpose (comment: avoid a CORS preflight).
4. Failure shows `data.message` or a generic login failure. The session is not stored.
5. Success shows the disclaimer. Only Accept calls `setUser`, writes the cookie and `localStorage`, refreshes `/me`, and sets `disclaimerAccepted` for 30 days.

Forgot Password opens `https://www.sawis.co.za/index.php`. Create One opens `https://www.sawis.co.za/register.php?l=en`. Neither is implemented in this repo.

### W3 — Sign out

Trigger: navbar control while logged in, confirm on `LogoutForm`, or a 401 from `checkUnauthorizedAndLogout`.

Behaviour: every JS-visible cookie is expired, and `localStorage` and `sessionStorage` are cleared. Capture drafts go with them.

### W4 — Export reports

Route `/reports`. No login guard on the route.

Sidebar filters apply to every tab except Price Range, which closes the sidebar (`Reports.js`).

Tabs:

| Tab | Who sees it | Data |
|---|---|---|
| Overview | Everyone | `POST /getChartData`, `/getYearOverYearComparison`, `/getTotale_Liters`, `/getExports`, `/getCultivarChartData`, `/getTop25Data` |
| Volume, or Volume / Value when logged in | Everyone | Table endpoints under `/tables/…`, prefixed with `/auth` when logged in |
| Value | Logged out only | Prompt to log in (`LogoutValue`) |
| Price Range | Control is always rendered | Logged-in calls to `/auth/getPriceRangeData` and filter history. Copy says up to five tables; the Create button styling changes when `PRFilters.length === 5` (`PricesRanges.jsx`) |
| Reports | Rendered with the other pills; scope doc says login-only | `POST /auth/reports/getRSAExportsReport` and `GET /auth/getCountryGroup` (`GeneratedReportPivotTable.jsx`) |

Filter list: `GET /getFilters`, or `GET /auth/getFilters` with `Authorization: <token>` when a user is present and `REACT_APP_USE_AUTH_HEADERS` is not `"false"`.

Changing user resets the active tab to Overview.

Excel export uses the table-to-excel / xlsx libraries already in the bundle. **Inferred** from those dependencies and the scope doc; this PRD did not trace every export button.

### W5 — Grape production and vineyard status

Routes are public.

1. Page loads a report list: `GET /getGrapeProductionReports` or `GET /getVineyardStatusReports`.
2. Sidebar loads `GET /getGrapeProductionFilters` or `GET /getVineyardStatusFilters`.
3. Generate Report on a card calls `item.URL`. Vineyard (`Type === "VineyardStatus"` or URL starting `vineyardstatus/`) uses GET with query parameters. Other cards POST JSON. Filter keys are lower-cased.

`scope/User_Flow_Scope.md` still says both use POST. The code does not.

### W6 — Domestic sales reports

Route `/domestic/*` is behind `AuthRouteGuard`. Anonymous users hitting it are sent to `/`. Navbar and Home hide the entry until login.

`/domestic` redirects to `/domestic/reports`. Unknown subpaths do the same.

The reports page loads `GET /getDomesticReportFilters` and generates with `POST /getDomesticReport`. Those two calls do **not** send `Authorization` (`domesticReports.js`), even though the page requires a session. **Requires confirmation** that the API allows that.

The Reports / SAWIS 7 capture tab bar is omitted entirely when `canCaptureDomestic` is false. The reports page itself still renders.

### W7 — SAWIS 7 capture

Route `/domestic/capture`. `AuthRouteGuard` already requires login. The page then requires `canCaptureDomestic`. Otherwise it shows: "You are not yet linked to a producer code — contact SAWIS / your Super User Admin".

Open a return: `POST /auth/domestic/submissions` with `PRODKODE`, `period`, and `country` when set. Role 4 types a code (Enter or blur looks it up). Other roles use a dropdown fed by `GET /auth/domestic/me/producer-codes` and assigned codes.

Front page: wine-type strip from lookup headings, then Step 1 package/size litres. Save draft `PUT …/saved-totals` with `version: 2`, `wine_classes`, `wine_class_totals`, `grand_total`. Draft save is not blocked by mismatch warnings. Submit is.

Back page: `GET/POST/PUT/DELETE …/submissions/{id}/data`.

Submit: client blocks until volumes exist, nothing is over-allocated, breakdowns are complete, front and back totals match, and the strip matches Step 1. Then `GET …/reconciliation` and `POST …/submissions/{id}/submit` with no body. The client does not PUT status.

Read-only in the producer form when status is `submitted`, `approved`, or `synced`. Cancelled is not in that list (see business rules).

A local draft is debounced into `localStorage` (`sawis_capture_draft_v1_<email>_<submissionId>`), restored with a banner, and cleared on submit. Logout clears all of `localStorage`.

Past submissions: `GET …/submissions/past?PRODKODE=`. View opens that id in a modal with context locked.

Help: `GET/POST /auth/domestic/help`. First-login text uses `domestic.first_login_help_text` when the API sends it. A 404 gap substitutes `HELP_SPEC_DATA`.

Litres inputs reject negatives and format two decimals on blur.

### W8 — Admin tools (mapper)

`/admin/tools` for every admin role except 3.

- History: `GET /admin/mapper_processing_sheet/history`.
- Upload: `POST /admin/mapper_processing_sheet_queue` for xlsx, xls, csv (scope doc; wizard component).
- Process detail, stats, retry, cancel: `/admin/process-thread/{id}`, `/admin/mapper_process_get_stats`, `/admin/mapper_processing_retry/{id}`, `/admin/mapper_processing_cancel`, `/admin/mapper_processing_final_sheet_test`.
- Lookups CRUD under `/admin/<endpoint>` for unknown cultivar, country, and producer matches, and MCC designation lookup (`MapperLookups.jsx`).

Producer list verify/sync (`POST` style calls in `domesticCapture.js`): `/auth/domestic/admin/verify-producer-list` and `sync-producer-list`. The sync panel text says roles 2/4.

### W9 — Admin users

`/admin/users` when `canManageDomesticUsers`.

- List/search: `GET /auth/domestic/admin/users?role=&q=`
- Detail: `GET …/users/{id}`
- Save assignment: `PUT …/users/{id}/assignment` with `domestic_producer` set to the **logged-in** user's id (not 0, and not the row being edited), plus codes and `admin_email`.
- Promote: `PUT …/role` with `admin_role: 3`. Hidden when the row is already role 3.
- Demote: `PUT …/role` with `admin_role: 0`.

A user is shown as Linked when `is_domestic_producer` and the code list is non-empty. Code `"0"` is treated as no codes.

### W10 — Admin submission review

`/admin/submissions` and `/admin/submissions/:submissionId`.

List filters period, status, producer code (`GET /auth/domestic/admin/submissions`).

Review loads `GET …/submissions/{id}`.

| Action | Who | When the button logic allows it | Request |
|---|---|---|---|
| Approve | `canApproveDomestic` | status submitted, approved, or open | `POST …/approve` body `{ sawis5_confirmed: true, note? }` |
| Unlock | same `canAct` gate, and not when status is open or synced | `POST …/unlock` body `{ reason: "sawis5_mismatch", unlock_reason, notify_producer: true }` |
| Cancel | roles 2, 3, 4 | open or submitted | `POST …/cancel` no body |
| Open up (reopen) | roles 2, 4 | cancelled or missed | `POST …/reopen` no body |

The approve flag is named `sawis5_confirmed` while the screen is labelled SAWIS 7. **Unknown / Requires Confirmation** what SAWIS 5 means here.

Footer sums: package/size volumes on one side, cultivar lines on the other, with a warning when they differ (`CHANGELOG.md` and review page).

## Functional requirements

### FR-001 — Public report routes

The system shows Home, `/reports`, `/grape-production`, and `/vineyard-status` without a session.

Evidence: `AppRoutes.js` (no guard on those routes). Implementation: **Implemented**. Confidence: High.

### FR-002 — Domestic routes require a session

`/domestic/*` redirects to `/` when `user` is null. Nav and Home omit Domestic Sales in that case.

Evidence: `AuthRouteGuard.jsx`, `Navbar.js`, `Home.js`. Implementation: **Implemented**. Confidence: High.

### FR-003 — Admin routes require admin_role > 0

Otherwise redirect to `/`.

Evidence: `AdminRouteGuard.jsx`, `canAccessAdminPortal`. Implementation: **Implemented**. Confidence: High.

### FR-004 — Role menus inside Admin

DAFF (1): Tools only. Super User (3): users and submissions, landing on submissions. Roles 2 and 4: Tools, users, and submissions, landing on Tools. Other admin roles that can open tools land on Tools.

Evidence: `domesticPermissions.js`, `Admin.js`. Implementation: **Implemented**. Confidence: High.

### FR-005 — Login then disclaimer

The session cookie is written only after the disclaimer is accepted.

Evidence: `LoginForm.jsx` `handleAcceptDisclaimer`. Implementation: **Implemented**. Confidence: High.

### FR-006 — Login body encryption off localhost

Non-local API bases send an AES payload. Empty base and localhost send `{ email, password, typeOfLogin }`.

Evidence: `shouldUsePlainLoginPayload`, `encryptLoginPayload.js`, README. Implementation: **Implemented**. Confidence: High.

### FR-007 — Session expiry is 401 only

A 403 or a body containing "denied" does not log the user out.

Evidence: `authSession.js`. `filterStore.js` comment still says 401/403. The comment is stale. Implementation: **Implemented**. Confidence: High.

### FR-008 — Export value data follows login

Logged-out Volume tab has no dollar icon and a separate Value tab. Logged-in label is "Volume / Value" and the Value prompt tab is hidden. Table and chart calls gain an `/auth` prefix when a user exists.

Evidence: `Reports.js`, `filterStore.js`. Implementation: **Implemented**. Confidence: High.

### FR-009 — Price range cap of five

The UI tells the user they can add up to five tables and changes the add control when five exist.

Evidence: `PricesRanges.jsx`. Implementation: **Implemented** (presentation). Whether the sixth `saveFilters` call is also rejected was not separately proven. Confidence: Medium.

### FR-010 — Vineyard generate uses GET

Grape and other non-vineyard pivot cards POST. Vineyard cards GET.

Evidence: `GenericPivotTable.jsx`. Conflicts with `scope/User_Flow_Scope.md` section 6. Implementation: **Implemented**. Confidence: High.

### FR-011 — Capture permission

Without `can_capture` (or the role fallback), `/domestic/capture` shows the unlinked message and does not mount the capture panel. The tab bar is hidden on domestic pages.

Evidence: `ProducerCapturePage.jsx`, `DomesticSalesTabs.jsx`. Implementation: **Implemented**. Confidence: High.

### FR-012 — Submission identity

Opening a submission sends producer code, period, and country when country is set.

Evidence: `openOrGetSubmission` in `domesticCapture.js`. README says the key is PRODKODE + period + country. Implementation: **Implemented** on the client. The server uniqueness rule is **Unknown** (not in this repo).

### FR-013 — Draft versus submit

Save draft persists totals whenever the API accepts them. Mismatches warn and do not abort the save. Submit stays disabled until the client checks in W7 pass.

Evidence: `handleSaveDraft`, `submitBlockReason` in `ProducerCapturePanel.jsx`. CHANGELOG matches. Implementation: **Implemented**. Confidence: High.

### FR-014 — Submit does not PUT status

Submit is `POST …/submit` with an empty body after a reconciliation GET.

Evidence: `submitSubmission`, changelog. Implementation: **Implemented**. Confidence: High.

### FR-015 — Producer read-only statuses

The capture form is read-only for submitted, approved, and synced unless `forceEditable` is set.

Evidence: `statusLocked` in `ProducerCapturePanel.jsx`. Implementation: **Implemented**. Confidence: High.

### FR-016 — Cancel and reopen audience

Cancel: roles 2, 3, 4, and only while open or submitted. Reopen: roles 2 and 4, and only while cancelled or missed.

Evidence: `domesticPermissions.js`, `SubmissionReviewPage.jsx`. Implementation: **Implemented**. Confidence: High.

### FR-017 — Assignment actor id

`domestic_producer` on assignment PUT is the signed-in user id (session, JWT payload, or users-list email match), never a hard-coded 0.

Evidence: `resolveAssignerId`, `resolveLoggedInUserId`. Implementation: **Implemented**. Confidence: High.

### FR-018 — Negative litres

Capture litre fields clamp to ≥ 0. Minus and pasted negatives are blocked.

Evidence: changelog and `handleChangeLitres` / `handleChangeCell`. Implementation: **Implemented**. Confidence: High.

### FR-019 — Period list

The capture period dropdown is the last 12 months from today's date.

Evidence: changelog "Capture period dropdown". This PRD did not re-read the date helper line by line. Confidence: Medium. Mark **Documented** in changelog; treat the exact date math as confirmed only to the level of that changelog entry plus the August 2026 commits.

### FR-020 — Dev server port

Local `npm start` listens on port 44487 (`PORT` in `.env.development`). Playwright `baseURL` is `http://localhost:44487`. README TestSprite section matches. CRA's own README text still says port 3000; that paragraph is the upstream CRA template.

Implementation: **Implemented** via env. Confidence: High.

## Integrations

| System | Direction | Trigger | Auth | Failure |
|---|---|---|---|---|
| BI API (`REACT_APP_API_BASE_URL` or dev proxy to `:8080`) | Browser → API | Page load, Apply, capture actions | `Authorization: <token>` on auth and admin calls. Public GETs for filters and domestic report filters have no header | 401 clears session. Other errors surface `message` from JSON. Network errors are thrown or shown as request failed |
| Google reCAPTCHA | Browser → Google | Login form, unless bypass env is true | Site key env | Sign-in button stays gated on a null captcha value when not bypassed |
| sawis.co.za | Browser navigation | Forgot password, Create account | none in this app | Ordinary link; no fallback in this repo |
| mailto `bi@sawis.co.za` | User's mail client | Navbar Contact | — | — |
| Google Analytics 4 | Browser → Google | 3 seconds after load (`src/index.js`) | Measurement id hard-coded in source | `initialize()` errors are logged; the app continues |
| SonarQube | CI on `master` | Bitbucket pipeline | `SONAR_TOKEN` variable | Pipeline file has no other branch |
| Deploy PHP | Server-side curl from `master_deploy.php` | Query `tkn` from an allow-listed IP | Token values not recorded here | Non-matching IP or token: "Nope" or empty default |

Backend database, queues, and workers are not in this repository. **Unknown**.

## Constraints

- SPA. Service worker registration is `unregister()` in `src/index.js`. Offline cache is not active.
- Auth token is stored in a JS-readable cookie and `localStorage`. There is no HttpOnly flag in `Cookies.set`.
- `DEVELOPER_DOCUMENTATION.md` is stale (see project plan). Do not treat it as the current architecture.
- Client lookup fixtures must not be read as production reference data.
- `master_deploy.php` contains live-looking tokens. They are intentionally omitted from this PRD.

## Error and empty behaviour (selected)

| Case | Behaviour | Evidence |
|---|---|---|
| Login HTTP error | Message from API or generic failure. No cookie | `authStore.login` |
| Disclaimer cancel | User is not `setUser`'d | `LoginForm.jsx` |
| `/me` 404 | Keep or rebuild domestic permissions. Not a logout | `refreshDomesticProfile` |
| Domestic route 404 with a JSON `message` | Real error (example: unknown PRODKODE), `gap: false` | `domesticRequest.js` |
| Domestic route 404 without a message | `gap: true`. Lookups and help then show spec fixtures | `domesticLookups.js` |
| Capture with no codes and not role 4 | Unlinked message | `ProducerCapturePage.jsx` |
| Submit while checks fail | Button disabled and a "You can still save a draft…" line | `userFacingMessages.js`, panel |
| Admin submission id missing | "not found" style message and a back link | `SubmissionReviewPage.jsx` |
| Export fetch while logged in that is not 401 | Session kept. Changelog cites a 404 against the CRA host that used to log people out | `authSession.js`, changelog |

## Non-goals of this document

This PRD does not specify the PHP or .NET API, the database schema, or a future phase. Those are outside `bitool_frontend` `phase_6`.
