# PRD — datafy_app (Datafy Ai)

Evidence date: 2026-10-18. Source: `git@bitbucket.org:overdrive/datafy_app.git`, `main` at `dba98cc7aa6f9868140511a98ef37240887539d0`.

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

No formal PRD existed in the repository or in `silicon_ark/datafy_app/` before this file. `README.md` titles the project "Datafy Ai" and says FlutterFlow projects run on the Flutter stable channel. That is the only product sentence in the README. Provenance: **Documented**, and it does not specify behaviour.

Requirements below are taken from the implementation. They are not an approved backlog.

## Product purpose

Datafy Ai is a Flutter client for retail store work. After sign-in, the person chooses a brand they are linked to, then works that brand's stores: stock tasks, promotions, KaChing challenges, and surveys. A role stored as exactly `Rep` uses screens that can run on PowerSync. Any other role value is sent to a parallel admin screen set that reads Supabase online.

Provenance: **Inferred** from `lib/main.dart`, `lib/pages/includes/auth/login/login_widget.dart`, `lib/pages/rep/dashboard/brands/brands_widget.dart`, and the route list in `lib/index.dart`. **Requires confirmation** as a product statement.

Names:

| Surface | Value | Evidence |
|---|---|---|
| MaterialApp title | Datafy Ai | `lib/main.dart` |
| Android label | Datafy | `android/app/src/main/AndroidManifest.xml` |
| iOS display name | Datafy | `ios/Runner/Info.plist` `CFBundleDisplayName` |
| iOS bundle name | Datafy Ai | `CFBundleName` |
| Web apple title / og:title | Datafy | `web/index.html` |
| Package | `datafy_ai` | `pubspec.yaml` |
| Application id | `coza.datafy.web` | `android/app/build.gradle`, iOS `PRODUCT_BUNDLE_IDENTIFIER` |
| URL scheme | `datafyai` | `ios/Runner/Info.plist` |

## Users and roles

| Role | What the client does | Evidence | Provenance |
|---|---|---|---|
| Signed-out person | `/` builds `LoginWidget`. The router error builder does the same when logged out | `nav.dart` | **Implemented** |
| Rep | `FFAppState().userRole == 'Rep'` opens the rep dashboard, store list, and notifications. The rep dashboard calls `initializePowerSync` | `brands_widget.dart`, `nav_bar_widget.dart`, `dashboard_widget.dart` | **Implemented** |
| Anyone else, including a role stored as `Admin` | Brand selection opens `DashboardAdmin`. The store nav icon opens the admin dashboard. Profile opens `ProfileNewAdmin` | same files, `else` branches | **Implemented** |
| Missing role on the email sign-in path | Defaults to the string `Rep` | `login_widget.dart` `valueOrDefault(..., 'Rep')` | **Implemented** |
| Missing role on the mobile-number sign-in path | The code reads `.role!` with no default | `login_widget.dart` | **Implemented**. Failure behaviour if the row or role is null was not executed |

`app_user.role` is copied into secure storage as `ff_userRole`. The comparison is exact. A value such as `admin` or `Manager` is not rewritten. It fails the `Rep` check and follows the admin branch. Provenance: **Implemented**. Whether those strings are real roles is **Unknown / Requires Confirmation**.

`app_user` also has `manager`, `super_admin` is on the separate `user` table binding (`user.dart`), and `archived`. The routed login path that was read does not branch on `super_admin` or `manager`. What those columns mean in the database is **Unknown / Requires Confirmation**.

There is no separate "customer" or "store manager" role in the client comparisons that were found.

## Major workflows

### WF-1 — Sign in

Trigger: person submits the login form.

Inputs:

- Mobile-number mode (`emailSignin` false): `phoneNumber` and password.
- Email mode: email text and password.

Behaviour (**Implemented**, `lib/pages/includes/auth/login/login_widget.dart`):

1. `authManager.signInWithEmail` is called in both modes. The mobile-number field is passed as the email argument.
2. If the auth user is null, the handler returns.
3. If location permission is not granted, a dialog says "Location disabled" / "Please enable your location." and the handler stops.
4. The client loads `app_user` where `id` equals `currentUserUid`.
5. Email mode stores `role` or `Rep`, and `fullName`. Mobile-number mode stores `role` and `fullName` without a default.
6. `checkGpsStatus` asks Geolocator whether the location service is enabled.
7. If it is enabled, the app navigates to `Brands`. If it is not, `openLocationSettings` runs.

Output: a Supabase session plus `userRole` and `userFullName` in `FFAppState`.

Error conditions observed in code: null auth user returns with no further navigation; missing location permission shows a dialog; GPS service off opens settings and does not navigate to Brands.

The exported login widget is `lib/pages/includes/auth/login/`. A second `LoginWidget` under `lib/pages/login/` is not exported from `lib/index.dart`. It contains `createAccountWithEmail`. It is not the routed screen. Provenance: **Implemented**.

`VerifyWidget` at `/verify` calls `resetPassword` from `lib/pages/login/verify/verify_widget.dart`. The routed verify export is `lib/pages/includes/auth/verify/`. Which of the two verify screens is reachable was not compared line by line. **Unknown / Requires Confirmation** which reset-password UI ships.

### WF-2 — Choose a brand

Trigger: tap a card on `/brands`.

Input: `customer_user` rows where `user_id` equals the current uid (`brands_widget.dart`). Empty list shows `NoItemsWidget`.

Behaviour:

1. Current location is read (default `LatLng(0, 0)` if unavailable).
2. `selectedCustomer` and `selectedBrand` are stored from the tapped row.
3. If role is `Rep`, insert `user_activity` with `type` `loggedIn`, `customer_id` equal to the selected customer, GPS string, and rep name, then open `/dashboard`.
4. Otherwise insert `user_activity` with `customer_id` literal `1` and the same other fields, then open `/dashboardAdmin`.

Provenance: **Implemented**. The literal `1` on the non-Rep path is not explained in a comment. **Requires confirmation.**

### WF-3 — Rep store work

Trigger: Rep opens the dashboard. `initializePowerSync` and `setUpAttachments` run on page load.

The nav bar then offers Home (`/dashboard`), Stores (`/storePageMain`), Profile (`/profileNew`), and Notifications (`/notifications`).

From stores the rep can open items, categories, task lists, task details, promos, KaChing, and surveys. Parallel `*Spar` and `*Admin` routes exist. `r_f_g_categories_widget.dart` navigates to `CategoryPageSpar`. The rule that chooses a Spar screen versus a non-Spar screen was not fully traced. **Unknown / Requires Confirmation.**

### WF-4 — Complete a task (Rep)

Trigger: button on `/taskDetails` (`lib/pages/rep/tasks/task_details/task_details_widget.dart`).

Input: choice-chip status (English strings in the localisation file are "Fixed" and "Not Fixed"), note text, last uploaded public URL, task id.

Behaviour:

- Whether PowerSync reports connected or not, the button calls `powersyncWrite` with `UPDATE task SET status = :status, image_url = :image, note = :note, archived = 1 WHERE id = :id`.
- The parameter map also sends `gps` and `user`. Those names are not in the SQL text.
- When connected, the same handler also inserts `user_activity` (`type` `task`) and a `product_images` row through Supabase, using the selected customer and the public image URL.
- Dialogs include "Partial update." / "Task was updated, but no image was uploaded." and "Update failed!" / "Please try again."

Provenance: **Implemented**. Archiving the task (`archived = 1`) on this update is behaviour in the SQL string, not a written rule.

Admin task details that were sampled update status with `UPDATE task SET status = :status WHERE id = :id` and do not set `archived` in that statement (`task_details_admin_widget.dart`). **Implemented.** Whether admins are supposed to archive is **Unknown / Requires Confirmation.**

### WF-5 — Complete a promo or KaChing item (Rep)

Trigger: buttons on the promo and KaChing detail screens.

Behaviour (**Implemented** SQL strings):

- Linked-user promo: `UPDATE promos_linked_users SET status, note, image_url, archived = 1`.
- In-store promo: `UPDATE store_promos SET status, note, image_url, archived = 1`.
- KaChing: `UPDATE kaching_linked_users SET status, note, image_url, archived = 1`.

English chip label sampled for promos and KaChing is "Complete".

The PowerSync schema and `StorePromoTable.tableName` use `store_promo`, not `store_promos`. Provenance: **Implemented** mismatch. Whether the in-store write succeeds is **Unknown** without a runtime test.

### WF-6 — Save a survey answer

Trigger: survey question form calls `saveSurveyResponse`.

Inputs: survey id, question id, user id, store code, answer type, answer. Answer types named in the action comment: string, number, boolean, choice, multiChoice, image.

Behaviour (`lib/custom_code/actions/save_survey_response.dart`):

- Returns null if any required argument is null, or if user id or store code is empty.
- Looks up `survey_responses` by survey, question, user, and store code.
- Updates the existing row or inserts one.

Provenance: **Implemented** and commented in the action. The comment is developer documentation inside the repo, not a separate PRD.

### WF-7 — Sign out

`ProfileNew` and `ProfileNewAdmin` call `authManager.signOut()`, which calls `SupaFlow.client.auth.signOut()`. Provenance: **Implemented**.

## Integrations

| System | Purpose | Direction | Auth | Evidence | Provenance |
|---|---|---|---|---|---|
| Supabase `https://datafy.appmonitor.co.za` | Auth, PostgREST, storage | Client to Supabase | Anon key hardcoded in `supabase.dart`. Header `X-Client-Info: flutterflow`. Auth flow `implicit` | `lib/backend/supabase/supabase.dart` | **Implemented**. The key value is omitted here |
| PowerSync `https://67c1c2c9586c8d282a0669f2.powersync.journeyapps.com` | Offline sync for the embedded schema | Client to PowerSync | Not described in this repo beyond the Supabase session the library uses | `lib/main.dart`, `dependencies/power_sync_b0w5r9` | **Implemented** |
| Supabase storage buckets `uploads` and `profile` | Task, promo, KaChing, survey, and profile images | Upload, then public URL | Same Supabase client | `storage.dart` and `bucketName:` call sites | **Implemented** |
| Geolocator / OS location settings | Gate sign-in and stamp activity | Device | OS permission | login widget, `check_gps_status.dart` | **Implemented** |
| WhatsApp / share sheet | `shareToWhatsApp` opens `wa.me` on web and the OS share sheet on Android/iOS | Outbound | None in code | `share_to_whats_app.dart` | **Implemented** |
| SonarQube `https://scan.appmonitor.co.za` | Static analysis on `main` | CI to scanner | `SONAR_TOKEN` pipeline variable | `bitbucket-pipelines.yml`, `sonar-project.properties` | **Implemented** |
| `alb.datafyapp.co.za` and FlutterFlow Storage | Fallback image URLs | Read | Public URLs | several widgets | **Implemented** as image fallbacks, not as an API client |
| GenAI / image model | Columns `genai_result`, `genai_message`, `processed_genai` on `task` and `product_images` | Unknown | Unknown | table bindings | **Unknown** which system writes them. This client inserts `product_images` rows and does not call an AWS URL that was found under `lib/` |

Failure behaviour for Supabase or PowerSync being down is mostly the widget's own dialogs (task update) or a spinner (`FutureBuilder` on brands). A global retry policy was not found. Provenance: **Unknown** beyond those local dialogs.

## Constraints

- Flutter stable channel is the only version constraint the README states. **Documented.**
- Dart SDK `>=3.0.0 <4.0.0`. Android `minSdkVersion` 23, `compileSdkVersion` 35, `targetSdkVersion` 35. **Implemented.**
- `publish_to: 'none'`. The package is not set up to publish to pub.dev. **Implemented.**
- Web `index.html` sets `robots` to `noindex`. **Implemented.**
- Location permission and an enabled location service are required before the routed login continues to Brands. **Implemented.**
- No route sets `requireAuth: true`. A person who is not logged in can still be sent to a named route if something navigates there; the redirect to `/login` only runs when `requireAuth` is true. **Implemented.**
- This PRD must not treat `flutter_app` behaviour as a requirement of `datafy_app`. See `project-plan.md`.

## Requirements discovered from the implementation

| ID | Statement | Who | Trigger | Output | Provenance |
|---|---|---|---|---|---|
| FR-001 | The app shows Login at `/` when there is no session, and Brands when there is | Any | Cold start or `/` | Login or Brands widget | **Implemented** |
| FR-002 | Sign-in uses Supabase email/password for both the email field and the mobile-number field | Person | Sign-in button | Session or a silent return | **Implemented** |
| FR-003 | Sign-in does not open Brands unless location permission is granted and the location service is enabled | Person | After a non-null auth user | Dialog, settings, or Brands | **Implemented** |
| FR-004 | Brands lists `customer_user` rows for the current user id | Signed-in person | Brands screen | Cards, or `NoItemsWidget` | **Implemented** |
| FR-005 | Choosing a brand stores `selectedCustomer` and `selectedBrand`, writes `user_activity` type `loggedIn`, and opens the rep or admin dashboard | Signed-in person | Card tap | Dashboard route | **Implemented**. Non-Rep `customer_id` is the literal `1` |
| FR-006 | The rep dashboard starts PowerSync and attachment setup | Rep | Dashboard first frame | Local database initialised | **Implemented** |
| FR-007 | A Rep can open stores, tasks, promos, KaChing, surveys, profile, and notifications from the routes in `lib/index.dart` | Rep | Nav and in-screen navigation | Those screens | **Implemented** |
| FR-008 | A non-Rep can open the admin twins of those areas | Non-Rep | Nav and in-screen navigation | `*Admin` routes | **Implemented** |
| FR-009 | Saving a rep task sets status, image URL, note, and `archived = 1` through PowerSync, online or offline | Rep | Task detail button | Local write | **Implemented** |
| FR-010 | When PowerSync is connected, that same task button also inserts `user_activity` and `product_images` via Supabase | Rep | Task detail button while connected | Two Supabase inserts | **Implemented** |
| FR-011 | Survey answers are upserted on survey, question, user, and store code | Rep (caller) | Survey form | `survey_responses` row or null | **Implemented** |
| FR-012 | Profile screens can sign the Supabase session out | Signed-in person | Sign-out control | `auth.signOut()` | **Implemented** |
| FR-013 | The client speaks English. Afrikaans is a registered locale with empty sampled translations | Any | Device locale | English strings | **Implemented** |
| FR-014 | `main` runs a SonarQube scan. The pipeline does not build or ship the app | CI | Push to `main` | Scan step | **Implemented** |

Nothing in the repository is marked **Planned**. A plan would need a document or an issue inside the repository, and there is neither.

## Data the product manages

The client does not own the database. It maps tables and a local PowerSync schema. Important groups:

- People and access: `app_user`, `customer_user`, `user_activity`, `user` (includes `super_admin`).
- Stores and stock: `store`, `store_user_link` (counts such as `oos`, `negative_stock`, `soh_no_sales`, `availability`, `fixed_count`), `product_store`, `product`.
- Work items: `task`, `promos`, `promos_linked_users`, `store_promo`, `kaching`, `kaching_linked_users`.
- Surveys: `survey`, `survey_questions`, `survey_multiple_choice`, `survey_store_link`, `survey_responses`.
- Images: `product_images`, `promo_gallery`, `kaching_gallery`, storage buckets.
- Local only: PowerSync table `attachments_queue`.

`store_user_link` also carries category text columns (`baking_aids`, `beans`, `beverages`, and others). What those strings mean operationally is **Inferred** as category state on a store, and **Requires confirmation.**

Backup and test bindings are generated too: `store_backup_20241118`, `store_product_backup_20241128`, `task_john`, `test_table`, `test_tenant`, `john_data`. Whether screens still query them was not fully searched. Treat them as schema surface, not as confirmed features.

## Edge cases visible in code

- Empty brand list: `NoItemsWidget`.
- Auth returns null: login handler returns.
- Location permission denied: dialog, no Brands navigation.
- Location service disabled: OS settings, no Brands navigation.
- Task image missing after an update attempt: "Partial update." dialog.
- Task update flag false: "Update failed!" dialog.
- Survey save missing ids, user, or store code: action returns null and `debugPrint`s.
- Location fallback coordinate is `0, 0` when `getCurrentUserLocation` has no fix. That value can be stored in `user_activity.gps`.
- Router loading state shows a full-screen asset image for about one second (`Future.delayed` 1000 ms) plus while `user == null`.

Provenance: **Implemented**.
