# PRD — flutter_webportal (Datafy Web)

Evidence date: 2026-10-14. Source: `git@bitbucket.org:overdrive/flutter_webportal.git`, `main` at `b98b9e2` (2026-09-24).

In-repo product scope lives in `docs/SCOPE.md` and per-page behaviour in `docs/FEATURE_REFERENCE.md`. This PRD restates what those documents and the current code agree on, and marks conflicts. It does not replace a decision where they disagree.

Provenance labels: **Documented** (written in repo docs or changelog), **Implemented** (present in code or SQL on this commit), **Inferred**, **Planned**, **Unknown**.

## Product purpose

Staff use a browser to operate Datafy for one retail customer at a time: see store and rep performance, manage users and stores, oversee tasks and surveys, run Specials and KaChing campaigns, and import or export Sjreport / SDC / week-report data.

The mobile app that reps use in store is not this repository (`docs/SCOPE.md`). Provenance: **Documented**.

Package: `datafy_web` `1.0.10+1`. UI stack: Flutter web, originally FlutterFlow, with a large custom-code layer.

## Users and roles

Roles are not an enum in the client. Login reads the signed-in row from the `"user"` table and stores `role` and `super_admin` on app state (`login_widget.dart`).

| Role signal | What the UI does with it | Provenance |
|---|---|---|
| `super_admin == true` → `FFAppState().superAdmin` | Extra nav items: Products, Special, KaChing, Surveys, Users, Customers, Notifications, Sjreport, Callsheet. Deactivate control on edit user. Waterwheel emergency stop. | **Implemented** (`navbar_widget.dart`, `edit_user_widget.dart`, `waterwheel_reports_control.dart`) |
| `user.role == 'Admin'` | SDC Report nav. Treated as admin in survey/promo/KaChing overview widgets (`userRole == 'Admin' \|\| userRole == 'SuperAdmin'`). | **Implemented** |
| `user.role == 'Manager'` | SDC Report nav. Survey/store RPCs document that managers see stores linked through `store_user_link`. | **Implemented** and **Documented** in action comments (`get_surveys_for_store.dart` and siblings) |
| `user.role == 'SuperAdmin'` | Same admin branch as Admin in several overview widgets, separate from the boolean `superAdmin`. | **Implemented**. How this string relates to the boolean is **Unknown** if they can diverge. |
| App user (rep) | Created and linked from the portal (`app_user`). Not the portal session user. | **Documented** in `docs/FEATURE_REFERENCE.md` and **Implemented** in add-user flows |
| Anonymous | `/` shows login when logged out. Other product routes set `requireAuth: true`. Unknown paths: logged-in users see the dashboard, others see login. | **Implemented** (`nav.dart`) |

`docs/SCOPE.md` says SuperAdmin is the only role that sees SDC Report. The navbar also shows it to Admin and Manager. The navbar is the **Implemented** rule. The scope table **Requires Confirmation**.

## Main workflows

### W1 — Sign in

Trigger: user opens `/` or `/login`.

Inputs: email and password (`login_widget.dart`).

Behaviour:

1. `authManager.signInWithEmail`.
2. Load `UserTable` where `id` equals the auth uid.
3. Copy name, email, profile image, `role`, and `superAdmin` into `FFAppState`.
4. Set `selectedCustomer` to `1`.
5. Navigate to `/dashboard`.

Forgot-password is on the same page (`resetPassword` is referenced by `docs/FEATURE_REFERENCE.md`). The sign-in steps above were read in `login_widget.dart`. Provenance: **Implemented**.

Error behaviour: if `signInWithEmail` returns null, the method returns and does not navigate. The exact snackbar text is inside `SupabaseAuthManager` / email auth and was not fully transcribed.

Output: dashboard. Session uses Supabase implicit flow (`supabase.dart`).

### W2 — Choose a customer and read the dashboard

Trigger: signed-in user on `/dashboard`.

Inputs: customer dropdown. Isolation rule (**Documented** in `docs/data-isolation.md`, **Implemented** as a required pattern): if `selectedCustomer <= 0`, do not call customer-scoped RPCs.

Behaviour the dashboard is built to show (**Documented** in `docs/SCOPE.md` and `docs/FEATURE_REFERENCE.md`, widgets present under `lib/custom_code/widgets/`):

- Overview metrics via `get_dashboard_overview`.
- Store score card and chart (`affects_scoring`, region filter; BOT and NAM excluded from scoring — **Documented** in `CHANGELOG.md` and migration `20250318200000_get_store_scores_exclude_bot_nam_regions.sql`).
- Active users and user statistics.
- Quick actions for SuperAdmin, including Sjreport upload.
- Waterwheel / Superset reports control (**Documented** in `CHANGELOG.md`: admins start the instance; reports open when EC2/Superset are ready; session auto-stops after one running hour; backup switch-off is superadmin-only).

Login currently forces customer id `1` (W1). Whether the dropdown then replaces that with a linked customer is **Implemented** in later screens that call `loadLinkedCustomerOptions()` (`docs/data-isolation.md`, changelog security note). The login assignment itself **Requires Confirmation**.

### W3 — Manage users

Trigger: SuperAdmin opens `/users` (`UsersManageTab`).

Behaviour (**Documented** in `docs/FEATURE_REFERENCE.md` and **Implemented** as pages under `lib/pages/users/`):

- Tabs for reps (`app_user`) and portal admins (`"user"`).
- Add rep/admin app user, or add portal Admin/Manager.
- Link a user to a customer (`customer_user`).
- Link a rep or manager to stores one-by-one or by region/channel. Bulk path calls `bulk_link_users_to_stores` and skips existing `(user_id, store_code, customer_id)` (**Documented** in changelog, migration `20260806150000`).
- Excel bulk import. Instructions sheet says Rep/Admin go to `app_user` (email optional) and Manager / Portal Admin / Web Admin go to `"user"` (email required) (`CHANGELOG.md`).
- SuperAdmin can deactivate via RPC `admin_deactivate_user` (sets user inactive, archives `app_user`, bans auth user — **Documented** in changelog, migration `20260422140000_user_deactivation.sql`).
- Delete uses Portal function `delete-user` (`delete_user_with_linked_items.dart`).

Creating an auth user must not replace the admin session. Changelog records a fix that restores the admin refresh session after `signUp`. Provenance: **Documented** and **Implemented** (`create_auth_user_preserving_session` named in the changelog).

### W4 — Stores

Trigger: any signed-in user opens `/stores` (nav item is not role-gated).

Behaviour: `StoresManageTab` lists stores with region/channel/search; add store; link reps. Store `region`, `channel`, and `store_code` are trimmed on write by DB triggers (**Documented**, migration `20260723130000`). `store_user_link.active` must be `1` for the UI view to show the link (**Documented**, migration `20260819060000`).

Admin vs manager store scope is enforced in RPCs, not only in the navbar. August 2026 migrations removed a `store_customer_link` existence gate that had collapsed large customers (example in changelog: RFG) down to the few rows present in that table. Current intended scope (**Documented** in those migration comments): derive store codes from `store_user_link` for the customer.

### W5 — Tasks

Trigger: `/tasks` (nav not role-gated).

Behaviour (**Documented** in `docs/FEATURE_REFERENCE.md`): filter by client, region, channel, store; tabs for not fixed, fixed, incomplete, and not fixed across stores; view and edit status plus proof image; export Excel/PDF/CSV. Optional image analysis posts `{ task_id }` to `{BEDROCK_ANALYSIS_API_URL}/analyze-image` (`docs/PROJECT_DOCUMENTATION.md`).

Portal task edit is administrative. The field fix flow is out of scope (`docs/SCOPE.md`).

### W6 — Surveys

Trigger: SuperAdmin nav item `/surveys`. The route itself only checks authentication, so a direct URL is not navbar-gated. **Implemented**.

Behaviour: overview and manage tabs; edit survey and questions; link to stores or users; view answers. Customer-scoped RPC `get_survey_response_detail` shows app submissions in the portal (`CHANGELOG.md`, migration `20260903100000`). Slide-overs are preferred over the standalone `/viewSurvey` route (`CHANGELOG.md`). Linked list requires `customer_id`.

Admin/SuperAdmin see the customer’s stores. Manager sees stores they are linked to. If the portal user’s role is only on `"user"` and not `app_user`, the RPC must fall back to `"user"` or the manager filter hides everything (**Documented**, migration `20260818100000`).

### W7 — Specials (promos) and KaChing

Trigger: SuperAdmin nav, routes `/promos` and `/kachingNew`.

Shared shape (**Documented** and **Implemented**): overview tab and campaigns tab; create/edit a master row; link to stores; Excel import via Storage plus a queue row plus a Datafy/Portal trigger.

KaChing rules that are explicit in code comments, changelog, and migrations (2026-09-15 to 2026-09-17):

- The store universe for linking is **active** `store_user_link` rows for the Campaigns-tab customer, not a customer inferred from `FFAppState`.
- Linking goes through `link_kachings_to_customer_stores` with `p_customer_id`.
- New assignments are `status = 'Incomplete'`, `archived = 0`.
- Unique key `(customer_id, kaching_id, lower(store_code))`.
- Excel import preflights dates, image URLs, and customer-scoped stores before writes. A confirmed unsupported image content type (for example `image/avif`) rejects the row. Network failures do not.
- A null `end_date` still counts as active. Expiring-soon still needs an end date within 7 days.
- Delete of a campaign removes gallery, assignments, and the master row (`delete_kaching_campaign`).
- First Complete freezes completer name, GPS, and `fixed_at`. Later saves update “last edited” only.
- Replacing the master image updates `default_image_url` on existing links and does not overwrite a rep’s submitted photo.

Specials follow the same campaign UI family (`promos_campaigns_tab.dart`, `campaign_link_panel.dart`). Promo-specific RPC names are listed in `docs/DB_FUNCTION_MIGRATION_MAP.md`.

### W8 — Products and non-issues

Trigger: SuperAdmin nav `/products`. Route `/nonIssues` exists for an SJ-style grid per store.

Behaviour (**Documented**): catalog manage tab; edit identity and image; uniqueness on category, brand, product, variant, size per customer; ML drawer (`product_ml`); category image import; product Excel import.

### W9 — SDC and week reports

Trigger: navbar “SDC Report” for Admin, Manager, or SuperAdmin, route `/sdc-report`.

Behaviour: `WeekReportChartWidget` calls combined week RPCs and Excel export. Upload of an SDC workbook goes through `upload_sdc_workbook.dart` → Portal slug `process-sdc-upload`. `DROS QTY` and `DAYS COVER` display rules are **Documented** in `CHANGELOG.md` (at most 2 decimal places; days cover comes from `sjreport.days_cover` and is null on task rows).

### W10 — Sjreport import

The working import is the dashboard upload, not the `/sjreport` page.

Behaviour (`README.md`, `docs/FEATURE_REFERENCE.md`, changelog): file to Storage prefix `uploads/sjreport/`, row in `sjreport_uploads`, then HTTP trigger `trigger-read-files` (direct Datafy URL or Portal proxy). HTTP 409 means the AWS side refused a re-trigger it considers already in progress or already imported. The portal shows an “Upload failed” dialog. The 409 decision is **not** in this repo (`CHANGELOG.md`).

`/sjreport` renders `SjreportPageTabs`, which shows the title “SDC Report” and `ComingsoonWidget`. The navbar Sjreport item does not navigate there. Provenance: **Implemented**. Product intent of that page: **Unknown / Requires Confirmation**.

### W11 — Notifications

`/notifications` is a customer-scoped `user_activity` feed with date, search, and read/unread (`docs/FEATURE_REFERENCE.md`). Navbar label text is `Notfications` (spelling in source). Nav item is SuperAdmin-only.

`notification_widget.dart` is a hard-coded mock and is not the live feed (**Documented**).

### W12 — Profile and logout

`/profile` is available to any signed-in user (nav label “Account”). Logout calls `authManager.signOut` and goes to `/login`.

### W13 — Callsheet

Route `/callsheet` is an empty shell (navbar, breadcrumbs, no body content) (`callsheet_widget.dart`). The navbar item opens `ComingsoonWidget` and does not push the route. **Implemented**: not a delivered feature. **Planned** or abandoned: **Unknown**.

### W14 — Self-service registration

`CreateAccountWidget` can call `createAccountWithEmail` and insert a user row (`docs/FEATURE_REFERENCE.md`). It is not registered in `nav.dart`. **Implemented** as dead route code. `docs/SCOPE.md` says self-service signup is secondary to admin provisioning. Whether the page should be reachable is **Unknown / Requires Confirmation**.

## Functional requirements

### FR-001 — Email sign-in

The portal authenticates with email and password against Supabase Auth and loads the matching `"user"` row before showing the dashboard.

- Trigger: Sign In on `/login`.
- Validation: Auth rejection returns no user and does not navigate (**Implemented**).
- Output: app state populated; route `/dashboard`.
- Evidence: `login_widget.dart`, `supabase_auth_manager.dart`.
- Implementation: **Implemented**. Confidence: High.
- Open: `selectedCustomer = 1` (see W1).

### FR-002 — Protected routes

Every product route except `/` and `/login` sets `requireAuth: true`. `/` renders the dashboard or login from the auth notifier.

- Evidence: `lib/flutter_flow/nav/nav.dart`.
- Implementation: **Implemented**. Confidence: High.

### FR-003 — Customer scope

Customer-scoped reads pass the selected customer id and do nothing when that id is `<= 0`.

- Evidence: `docs/data-isolation.md`, `lib/custom_code/customer_scope.dart` (named by that doc).
- Implementation: **Documented** as a required pattern; individual call sites were not all re-audited in this pass. Confidence: Medium.
- Server enforcement (RLS) is environment-dependent. See business rules and the project plan.

### FR-004 — Role-based navigation

The sidebar shows Dashboard, Tasks, Stores, Account, and Logout to every signed-in user. It shows Products, Special, KaChing, Surveys, Users, Customers, Notifications, Sjreport, and Callsheet only when `superAdmin` is true. It shows SDC Report when role is Admin or Manager, or when `superAdmin` is true.

- Evidence: `navbar_widget.dart`.
- Implementation: **Implemented**. Confidence: High.
- Note: route `requireAuth` does not repeat these role checks. A bookmarked URL may still open a page the nav hides. Whether RPCs then reject the call is per-function. **Requires Confirmation** as a product rule.

### FR-005 — User provisioning and linking

Admins create app users and portal users, link them to customers and stores, import them from Excel, and (superadmin) deactivate them. Delete goes through Portal `delete-user`.

- Evidence: `lib/pages/users/`, `delete_user_with_linked_items.dart`, changelog, `docs/FEATURE_REFERENCE.md`.
- Implementation: **Implemented**. Confidence: High for presence of the flows; Medium for every validation message.

### FR-006 — Store and task operations

The portal lists and updates stores and tasks for the selected customer, exports task files, and can request Bedrock analysis of a task image when `BEDROCK_ANALYSIS_API_URL` is set.

- Evidence: `stores_manage_tab.dart`, `tasks_manage_tab.dart`, `docs/PROJECT_DOCUMENTATION.md`.
- Implementation: **Implemented**. Confidence: Medium (page shells confirmed; every export branch not re-read line by line).

### FR-007 — Campaigns

Specials and KaChing support master records, customer-scoped store linking, overview, completion exports, and workbook import.

- Evidence: `lib/pages/promos/`, `lib/pages/kaching/`, `lib/custom_code/widgets/kaching_campaigns_tab.dart`, migrations `20260915*` and `20260916*` / `20260917*`.
- Implementation: **Implemented**. Confidence: High for the KaChing rules named in W7.

### FR-008 — Surveys

The portal manages survey definitions, store/user links, and response viewing for the selected customer, with manager store-link filtering.

- Evidence: `lib/pages/surveys/`, `get_surveys_for_store.dart`, migration `20260903100000`.
- Implementation: **Implemented**. Confidence: Medium.

### FR-009 — Reports and imports

Sjreport workbooks import from the dashboard via Storage and a Datafy trigger. SDC week reports render on `/sdc-report` and accept workbook upload through `process-sdc-upload`. Combined week RPCs back the chart and Excel export.

- Evidence: `uploadfile.dart`, `upload_sdc_workbook.dart`, `week_report_chart_widget.dart`, `datafy_api_client.dart`.
- Implementation: **Implemented**. Confidence: High for the trigger names; the Lambda’s 409 policy is outside this repo.

### FR-010 — Activity feed

`/notifications` lists `user_activity` for the customer and can mark rows read.

- Evidence: `docs/FEATURE_REFERENCE.md`, `notifications_widget.dart` route.
- Implementation: **Documented**, route **Implemented**. Confidence: Medium.

### FR-011 — External reports session

Dashboard Reports can start a Waterwheel/Superset session through Portal `waterwheel-control`. Documented behaviour: reports open only when the instance is ready; auto-stop after one running hour; emergency stop for superadmins.

- Evidence: `waterwheel_client.dart`, `waterwheel_reports_control.dart`, `CHANGELOG.md`.
- Implementation: **Implemented** client; the EC2 behaviour is in the Portal/AWS stack (**not fully verified here**). Confidence: Medium.

### FR-012 — Placeholders that must not be tested as features

- Callsheet page and navbar Callsheet item.
- Navbar Sjreport item (modal) and `/sjreport` coming-soon body. Real Sjreport import is the dashboard upload.
- `notification_widget.dart` mock panel.
- `edit_ka_ching_copy_copy` described as a stub in `docs/FEATURE_REFERENCE.md`.

Provenance: **Implemented** empty/coming-soon UI; **Documented** in `docs/SCOPE.md` and `docs/FEATURE_REFERENCE.md`.

## Integrations

| System | Direction | Trigger | Data | Auth | Failure |
|---|---|---|---|---|---|
| Supabase Auth | Portal → Supabase | Login, sign-up, sign-out, password update | Email, password, JWT | Anon key + user JWT | Snackbar from `AuthException` in `supabase_auth_manager.dart` |
| PostgREST / RPCs | Portal → Postgres | Page loads and saves | Customer-scoped rows and RPC params | User JWT | Empty UI when customer id `<= 0` (**Documented**). HTTP 409 on duplicate links is handled in bulk-link and import paths (**Documented** in changelog) |
| Storage `uploads` | Portal → Supabase | Imports and images | Objects under `sjreport/`, `promos/`, `kaching/`, `sdc/`, `users/` | User JWT | Upload error dialogs vary by caller |
| Portal Functions | Portal → API Gateway | Delete user, SDC upload, set phone, reverse geocode, Waterwheel, Datafy proxy | JSON body, slug path | Session JWT. Unsigned call throws `not signed in` | `StateError` / HTTP status surfaced by the caller |
| Datafy SAM | Portal → API Gateway | After queue insert | Trigger read-files or read-promos-kaching | **Unknown** beyond the URL define | 409 explained in the Sjreport upload dialog |
| Bedrock SAM | Portal → API Gateway | Analyze task image | `{ task_id }` | **Unknown** (URL define) | **Unknown** if the define is empty |
| Superset / Waterwheel | Browser → external host | Dashboard Reports | Login page `https://superset.waterwheel.datafy.co.za/login/` | Separate from portal session (**Inferred** from it being an external login URL) | Control widget gates the iframe on instance readiness |
| Google Maps | Browser links from activity/visit UI | User opens an address | Coordinates / address | None in this repo | **Documented** in `docs/SCOPE.md` only |

Portal slugs found in Dart: `delete-user`, `process-sdc-upload`, `admin-set-user-phone`, `reverse-geocode`, `waterwheel-control`, `trigger-datafy-read-files`, `trigger-datafy-read-promos-kaching`.

If `PORTAL_FUNCTIONS_API_URL` is unset, `portal_functions_client.dart` is documented to fall back to `functions.invoke` (`docs/PROJECT_DOCUMENTATION.md`). That fallback was not re-read line by line. Confidence: Medium.

## Constraints

- Online only. No offline product scope (`docs/SCOPE.md`).
- Anon key ships in the web bundle (`docs/SCOPE.md`).
- SPA hosting must rewrite 403/404 to `index.html` (`DEPLOY_AWS.md`, `web/serve.json`).
- Flutter web semantics are forced in CI (`E2E_SEMANTICS=true`) so TestSprite can see the DOM (`CHANGELOG.md`).
- Schema is shared with the mobile app. Migrations in this repo are not the only schema source (`docs/PROJECT_DOCUMENTATION.md`).

## Non-goals

See project plan. Callsheet business UI, dedicated Sjreport page content, and public signup as the default provisioning path are explicitly out of the delivered product in `docs/SCOPE.md`, and the code matches that for Callsheet and the Sjreport page.
