# PRD — formulys_simpleflow

Evidence date: 2026-10-14.

Source: `git@bitbucket.org:overdrive/formulys_simpleflow.git`, `master` at `07cd01b` (2024-06-21).

There is no prior PRD in the source repository. Silicon Ark had no `formulys_simpleflow/` directory before this file. `README.md` is the Vite template and does not state requirements. Requirements below are recovered from the React source, `package.json`, `bitbucket-pipelines.yml`, and `sonar-project.properties`. Provenance is labelled. Secret values are not in git.

The committed `src/App.jsx` does not parse (see FR-001). Behaviour described for screens is what the modules implement once that file can load. It was not executed in a browser during this inspection.

## Product purpose

**Documented:** the HTML title is "SimFlow Process Hub". Nothing else in git states a product purpose.

**Implemented.** A signed-in user sees a side navigation, a welcome header, and one of: Dashboard, Process Hub, Task List, Notification, Message, Reporting, Settings, or Management. Process definitions, company branding, users, and roles come from Form.io at `https://rzkiqpzlxxtfqjl.form.io`.

**Inferred.** The app is the Formulys client for starting and completing configured business processes and tasks, scoped by company and role.

**Unknown / Requires Confirmation:** product owner, customer, live URL, and the relationship to `formulys_simpleflow_backend` (see the project plan). This front end does not call that backend.

## Users and roles

| Actor | Evidence | Status |
|---|---|---|
| Anonymous visitor | `App.jsx` redirects to `/login` when the auth store `user` is null | **Implemented** |
| Signed-in user | Any user record returned by Form.io login or `GET /current` | **Implemented** |
| Simflow Admin | Literal `user.userRole.roleName === "Simflow Admin"` | **Implemented**. Commit `15b5186` renamed the earlier Admin label to this string |
| Project Manager | Literal `user.userRole.roleName === "Project Manager"` | **Implemented** as a second privileged role |
| Other roles | Options loaded from Form.io `simflowuserroles` | **Implemented** as data-driven names. The list is not in git. **Unknown** which names exist |
| Help seeker | Nav link `mailto:support@formulys.com` | **Implemented** as a mailto. No in-app help desk |

No role catalogue is hardcoded beyond the two literals above. A non-admin's role dropdown hides any Form.io role whose `roleName` is `Simflow Admin`.

The client does not read `isActive` during login. Whether Form.io rejects an inactive user is **Unknown / Requires Confirmation**.

## Systems the product talks to

| System | Direction | Trigger | Data |
|---|---|---|---|
| Form.io `https://rzkiqpzlxxtfqjl.form.io` | Browser → Form.io | Login, session restore, company load, lists, creates, updates, form render | JSON submissions. Auth header `x-jwt-token` |
| SendGrid `/v3/suppression/bounces` and `/v3/suppression/blocks` | Browser → SendGrid | Simflow Admin opens the Emails tab | API key from `import.meta.env.VITE_REACT_SENDGRID_API_KEY`. Query `from_email="joshua@overdrive.co.za"`, `limit` 10 |
| jsDelivr | CDN → browser | Every page load | Bootstrap 4.6.2 CSS/JS, Bootstrap Icons 1.11.3, jQuery 3.5.1 slim, Popper 1.16.1 |
| cdn.form.io | CDN → browser | Every page load | `formio.full.min.css` (unpinned) |
| FTP server named by `FTP_HOST_QA` | Pipeline → FTP | Push to `master` after a successful `npm run build` | Contents of `dist/*` |
| SonarQube at `SONAR_HOST_URL` | Pipeline → Sonar | Same pipeline, after the FTP step | Project key `overdrive_formulys_simpleflow_09e31949-1f76-42c8-9767-83697088df34` |

There is no database client, queue, or worker in this repository.

The same Form.io host is documented for `pestelite_html`. Paths here are `simflow*`. Whether that share is intentional is **Unknown / Requires Confirmation**.

## Routes

Defined in `src/App.jsx`. Shell routes render inside `Page` (side nav + header). `/login` does not.

| Path | Screen | Who can open it from the nav |
|---|---|---|
| `/login` | `LoginForm` | Everyone (also the redirect target when `user` is null) |
| `/:cardtype?/:id?` | `Dashboard` | "Dashboard" links to `/` |
| `/processHub` | `ProcessHub` | Every signed-in user |
| `/taskList` | `TaskList` | Every signed-in user |
| `/notification` | `Notifications` → "Coming Soon" | Every signed-in user |
| `/message` | `Messages` → "Coming Soon" | Every signed-in user |
| `/reporting` | `ReportingPage` → "Coming Soon" | Every signed-in user |
| `/settings` | `Settings` → "Coming Soon" | Nav link only when role is Simflow Admin. The route itself has no role check |
| `/management` | `Management` | Nav link when role is Simflow Admin or Project Manager. The route itself has no role check |

`cardtype` and `id` are optional path params on the dashboard route. `Dashboard.jsx` does not read them. Provenance of the route shape: **Implemented**. Provenance of an intended deep link: **Unknown**. The unmerged branch `url-query-state` uses query params `formId` and `submissionId` instead, and is not on `master`.

React Router v6 ranks static segments above dynamic ones, so `/processHub` and the other literal paths should win over `/:cardtype?`. That ranking was not runtime-tested. Provenance: **Inferred** from the library version in `package.json` (`react-router-dom` `^6.22.1`).

A direct visit to `/management` or `/settings` by a signed-in user who lacks the nav link still mounts the page. Management then renders no tabs. Settings still shows "Coming Soon". Provenance: **Implemented**.

## Major workflows

### WF-1 — Log in

**Implemented** in `LoginForm.jsx` and `authStore.js`.

Trigger: submit the login form, or load `/login` when `localStorage` already has `token`.

Inputs: email (`type="email"`) and password (`type="password"`). The login POST body is `{ data: { email, password } }`.

Steps on submit:

1. POST `https://rzkiqpzlxxtfqjl.form.io/simflowlogin/submission` with header `x-jwt-token` set to an empty string.
2. On success, store `response.headers["x-jwt-token"]` under `localStorage` key `token`.
3. Set `user` to `{ ...response.data.data, _id: response.data._id }`.
4. GET `simflowclientdetail/submission/{user.company._id}` with the token and store the company submission.
5. Navigate to `/`.

On failure the form shows "Could not login, please try again" and logs the error. No lockout, captcha, or password-reset link exists in this repo.

If a token is already stored, `autoLogin` GETs `{FORMIO_BASE_URL}/current` and then loads the company the same way. Failure is logged. The token is not removed. `user` stays null, so `App` sends the visitor to `/login`, and the next visit tries the same token again.

### WF-2 — Stay signed in and log out

**Implemented.**

While `user` is null, an effect in `App` calls `navigate("/login")`. The effect depends on `[user]` and does not list `navigate`.

Logout (nav button) removes `token` and sets `user` and `company` to null. The effect then returns the visitor to `/login`.

### WF-3 — See the dashboard

**Implemented** in `Dashboard.jsx`, `submissionStore.js`, and `DashboardProcessCard.jsx`.

Trigger: `company` becomes available.

1. `getAllSubmissions` keeps processes where `roleAccess` contains the user's `roleName`. It does **not** apply the Simflow Admin bypass used elsewhere.
2. For each kept process it GETs `{businessProcessForm.formUrl}/submission/?limit=500`, optionally with extra query options.
3. Each submission row is copied onto the store with `isTask`, `displayedIcon`, `businessProcessForm`, `formProcess` (the process name), and `formName: "Card Title"`.
4. The grid shows rows whose `data.simflowCardName` contains the search box text (case-insensitive). Rows with no `simflowCardName` are omitted. The search box starts empty, so the empty string matches every named card.
5. A separate fetch, which **does** include Simflow Admin, builds "Recent Submissions", sorts by `modified` descending, then sets the array length to 15. That panel renders only for Simflow Admin.

Opening a card opens a Bootstrap modal whose body is a Form.io `<Form>` pointed at `{formUrl}/submission/{_id}`. The close handler calls `getAllSubmissions`, which is not defined in `DashboardProcessCard.jsx`. Provenance: **Implemented** defect. Closing a card after a successful submit will throw.

Card colour: green `#00bf63` when `simflowCardStatus` is truthy; orange `#ff9900` when status is falsy and `isTask` is set; otherwise the first assignment leaves it green.

The chips "For You", "Team", "Department", "In Progress", and "Following" are styled spans. They have no click handler. Provenance: **Implemented** as labels only.

The header search input is commented out. The nav search input has no handler.

### WF-4 — Start a process

**Implemented** in `ProcessHub.jsx` and `BusinessProcessCard.jsx`.

The hub lists `company.data.businessProcesses1` entries where `isTask` is falsy and (`roleAccess` contains the user's role **or** the user is Simflow Admin). An empty `roleAccess` array does not match, including for Simflow Admin, because the admin check sits inside `Array.some`.

Each card opens a modal and renders `<Form src={businessProcessForm.formUrl}>`. On submit-done the code removes `.null.has-success` from the document, toggles the Bootstrap modal, and reloads submissions. Company colours `primaryColor`, `secondaryColor`, and `tertiaryColor` style the card, with fallbacks `#6e7982`, `#a6a6a6`, and `#ff9900`.

Process names and form URLs are company data in Form.io. They are not in git. **Unknown** which processes exist.

### WF-5 — Work a task

**Implemented** in `TaskList.jsx` and the nav badge in `Navigation.jsx`.

The task page calls `getAllSubmissions` (same role filter as the dashboard grid, no Simflow Admin bypass) and shows rows where `isTask` is set, `simflowCardStatus` is falsy, and `simflowCardName` contains the search text.

If `isTask` is set, status is falsy, and `simflowCardName` is missing, the filter calls `.toLowerCase()` on a missing value and throws. The dashboard search guards that case. The task list does not. Provenance: **Implemented** difference. Commit `3cf9e9f` ("Log message when simflowCardName missing") did not add a guard here.

The nav badge counts the same open-task condition with no search filter. It does not guard a missing `simflowCardName` before reading `.data`, but it does not call `.toLowerCase()`.

`TaskList` sets `loading` false in a `finally` that runs before `getAllSubmissions` resolves. The spinner therefore does not cover the fetch. Provenance: **Implemented**.

### WF-6 — Update my profile

**Implemented** in `UpdateUserForm.jsx`, opened from the nav "My Profile" button and from the header name, both via Bootstrap modal `#userProfileModal`.

Fields: first name (required), last name (required), role (disabled), email (disabled), password (required), confirm password (required).

If the two passwords differ, the form sets "Passwords must match" and does not submit.

Otherwise PUT `https://rzkiqpzlxxtfqjl.form.io/simflowclientusers/submission/{user._id}` with `data` equal to the current `user` object spread, plus `firstName`, `lastName`, and `password`. Then `autoLogin` with the existing token. Errors are logged. The modal is not closed by this handler.

The profile URL is a string literal. Other Form.io calls use `FORMIO_BASE_URL`. Both currently point at the same host.

A profile save always sends a password because the inputs are `required`. There is no "leave password unchanged" path on this form. The user-management form is different (WF-8).

### WF-7 — Manage companies

**Implemented** in `CompanyManagement.jsx`. Visible as the Companies tab when the role is Simflow Admin or Project Manager.

List source: GET `simflowclientdetail/submission/?limit=500`. Simflow Admin receives the unfiltered list. Anyone else sends `data.businessName=` plus the signed-in company's `businessName`.

Columns: Business, Contact (`contactPersonEmail`), Active (`isActive` truthy → "True", else "False"). Search matches business name or contact email. The add control (plus icon) renders only for Simflow Admin.

Create opens Form.io form `simflowclientdetail`. Edit opens `simflowclientdetail/submission/{id}`. For a role other than Simflow Admin the Form.io option `disabled.businessName` is true (commits `69afe58`, `fe3c6c9`). After submit the list reloads and the modal closes.

The Active cell has no click handler. Business and Contact do.

If `businessName` is present and `contactPersonEmail` is missing, the search `.toLowerCase()` throws. Provenance: **Implemented** risk. Not runtime-tested.

### WF-8 — Manage users

**Implemented** in `UserManagement.jsx` and `UpdateUserAccountForm.jsx`. The Users tab is available to Simflow Admin and Project Manager. The add control is not limited to Simflow Admin, so a Project Manager who can see the tab can open the create form.

List source: GET `simflowclientusers/submission/?limit=500`. Non-admins add `data.company.data.businessName=` plus their company name.

Columns: Name, Email, Company, Role, Active. Search matches first name, last name, email, or company business name. A row click opens the edit modal.

The form loads roles from `simflowuserroles/submission/` and companies from `simflowclientdetail/submission/` (non-admins filtered to their business name, and without the Simflow Admin role). Create POSTs `simflowclientusers`. Update PUTs `simflowclientusers/submission/{id}`.

Payload fields: `firstName`, `lastName`, `email`, `userRole: { roleName }`, `company` (the whole company submission object), `isActive`. `password` is included only when the password state is not the boolean `false`. The password input is not `required` (commit `22a9d07`). Initial React state is `false`, so an untouched password is omitted.

If the edited user is the signed-in user, the handler calls `autoLogin` and does not call `onSubmitDone`. Otherwise it calls `onSubmitDone`, which reloads the list and closes the modal.

`getUserAcc` chooses the current role by reading the `allRoles` React state immediately after `getRoles` returns. `setAllRoles` has not re-rendered yet, so the loop can see an empty list. Provenance: **Inferred** defect from the call order. Not runtime-tested.

`src/pages/UserManagement.jason.jsx` is an older Form.io-builder version of this screen. Nothing imports it. Provenance: **Implemented** leftover. Commit `b843b82` is the rewrite that replaced the builder.

Commit `997a2a3` says "Look into deleting users". No delete control exists on `master`.

### WF-9 — Review email suppressions

**Implemented** in `EmailManagement.jsx`. The Emails tab renders only when the role is Simflow Admin.

On mount the page reads `VITE_REACT_SENDGRID_API_KEY` and calls the SendGrid client for bounces and blocks. Each request's query string is `from_email="joshua@overdrive.co.za"` and `limit` 10. Results render as Status, Reason, Email, and Created (`created` UNIX seconds formatted `YYYY-MM-DD`).

Failures are logged. `setLoading(false)` runs in `finally` before the request promises resolve, so the spinner does not cover the response. Empty arrays render empty tables. There is no retry and no action to remove a bounce or block.

`example.env` does not define this variable. A comment in the file mentions `VITE_SENDGRID_API_KEY`; the code does not read that name.

Whether the suppression endpoints accept that `from_email` query is **Unknown / Requires Confirmation**. The client sends it.

### WF-10 — Build and publish on master

**Implemented** in `bitbucket-pipelines.yml`. Not observed running.

Trigger: a pipeline for branch `master`.

1. Image `node:20`, `npm install`, then `CI=false npm run build`. Artifacts: `dist/**`.
2. Install `ncftp` and `ncftpput` `dist/*` to `$FTP_HOST_QA` / `$FTP_SITE_ROOT_QA` with `$FTP_USERNAME_QA` and `$FTP_PASSWORD_QA`.
3. Echo "Finished uploading /build files to $FTP_HOST$FTP_SITE_ROOT". Those two names are not the variables used in the upload.
4. SonarQube pipe `sonarsource/sonarqube-scan:2.0.1` with `SONAR_HOST_URL` and `SONAR_TOKEN`. Cache `~/.sonar`. Clone depth is `full`.

The YAML comment says `npm install` is "required to install zip". The script does not install zip and does not zip the build.

Step 1 cannot succeed on revision `07cd01b` because `src/App.jsx` does not parse. Provenance: **Implemented** source defect since `5952e5d` (2024-05-09).

No pull-request pipeline, tag pipeline, or branch other than `master` is defined.

## Functional requirements

### FR-001 — The app module graph must load

**Implemented, and currently broken.**

`src/App.jsx` line 7 is `import Messages from "./pages/Messages";1`. The trailing `1` is a syntax error. `git blame` attributes that line to `5952e5d` (Jason Wandrag, 2024-05-09, "Updates"). It is still the line at `07cd01b`.

Evidence: `src/App.jsx`. Consequence: Vite cannot transform the module, so `npm run dev`, `npm start`, and `npm run build` fail before any screen renders. This inspection did not run the bundler.

### FR-002 — Authenticate against Form.io and keep a JWT

**Implemented** in `src/stores/authStore.js` and `src/pages/LoginForm.jsx`.

- Login URL: `{FORMIO_BASE_URL}/simflowlogin/submission`.
- Session URL: `{FORMIO_BASE_URL}/current`.
- Company URL: `{FORMIO_BASE_URL}/simflowclientdetail/submission/{user.company._id}`.
- Token storage: `localStorage["token"]`.
- Request header on later calls: `x-jwt-token`.

`FORMIO_BASE_URL` is `https://rzkiqpzlxxtfqjl.form.io` in `src/lib/constants.js`.

### FR-003 — Redirect unsigned users to login

**Implemented** in `src/App.jsx`. When `user` is null, navigate to `/login`.

### FR-004 — Show company branding

**Implemented** in `Navigation.jsx` and `BusinessProcessCard.jsx`.

If `company.data.logoUrl` is set, the nav image is that URL. Otherwise `src/assets/logo.png`. Process cards use `primaryColor`, `secondaryColor`, and `tertiaryColor` with the fallbacks named in WF-4. Login always uses `logo.png`, not the company logo.

The page background image is `src/assets/background.png` via `src/App.css`.

### FR-005 — List submissions the user's role may see

**Implemented** in `src/stores/submissionStore.js` and `src/lib/helpers.js`.

`fetchSubmissions` GETs `{formURL}?limit=500`, appends `&{options}` when `options` is not `""`, and sends the JWT. No token: return without a request. Error: `console.error`, return `undefined`.

The store filter compares `process.roleAccess[].roleName` to `user.userRole.roleName` only.

**Discrepancy (implemented):** `ProcessHub.jsx` and the dashboard's recent-items fetch treat Simflow Admin as allowed on every process that has at least one `roleAccess` entry. The shared store does not. The dashboard grid and the task list use the store, so a Simflow Admin sees those grids only for processes that list Simflow Admin in `roleAccess`.

### FR-006 — Open a submission or a blank process form in a modal

**Implemented** with `@formio/react` `<Form>` inside Bootstrap 4 modals. Modals are driven by `data-toggle` / `data-target` and jQuery `$().modal`, which depends on the jQuery and Bootstrap scripts in `index.html`.

### FR-007 — Count open tasks

**Implemented** in `Navigation.jsx`.

An open task is a stored submission with `isTask` set and `data.simflowCardStatus` falsy. The count is a badge on "Task List".

### FR-008 — Restrict management navigation

**Implemented** in `Navigation.jsx` and `Management.jsx`.

| Surface | Simflow Admin | Project Manager | Other signed-in role |
|---|---|---|---|
| Settings nav link | Shown | Hidden | Hidden |
| Management nav link | Shown | Shown | Hidden |
| Companies tab | Shown | Shown | Not rendered |
| Users tab | Shown | Shown | Not rendered |
| Emails tab | Shown | Hidden | Hidden |
| Add company | Shown | Hidden | Hidden |
| Edit company name | Enabled | Disabled in the Form.io options | Disabled |
| Add or edit user | Shown | Shown | Not rendered |
| Assign role Simflow Admin | Role is in the dropdown | Role removed from the dropdown | Not rendered |
| Company and user lists | All Form.io rows (limit 500) | Filtered to the signed-in business name | Not rendered |

Routes are not wrapped in a role guard. Hiding a link is the only client restriction. Form.io's own permissions, if any, are **Unknown / Requires Confirmation**. They are not expressed in this repo.

### FR-009 — Edit the signed-in profile without changing role or email

**Implemented** in `UpdateUserForm.jsx`. Role and email inputs are `disabled`. First name, last name, and both password fields are `required`. Passwords must be equal before PUT.

### FR-010 — Show SendGrid bounces and blocks to Simflow Admin

**Implemented** in `EmailManagement.jsx`. See WF-9. No other mail feature (compose, templates, send) exists in this repo.

### FR-011 — Placeholder pages say Coming Soon

**Implemented** in `ComingSoon.jsx`, used by `Notifications.jsx`, `Messages.jsx`, `ReportingPage.jsx`, and `Settings.jsx`. The only content is a heading "Coming Soon". Commit `d78591c` (2024-03-08) added these pages. Commit `e11b604` (2024-05-31, "Management styling and reporting") did not replace the reporting placeholder.

Provenance of a future build: **Inferred** from the heading and the commit message. No schedule is in git.

### FR-012 — Build with Vite and deploy `dist` from master

**Implemented** as pipeline YAML. `package.json` scripts:

| Script | Command |
|---|---|
| `start` | `vite` (Vite's default port) |
| `dev` | `vite --port 4000` |
| `build` | `vite build` |
| `lint` | `eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0` |
| `preview` | `vite preview` |

`vite.config.js` enables `@vitejs/plugin-react` only. It does not set `base`, a proxy, or env loading of its own. Vite's default env loading of `VITE_*` from `.env` is assumed and was not runtime-tested.

### FR-013 — Identify the Sonar project

**Implemented.** `sonar-project.properties` sets only `sonar.projectKey=overdrive_formulys_simpleflow_09e31949-1f76-42c8-9767-83697088df34`. No sources, exclusions, or quality gate are set in the file.

## Data the UI expects

These fields are read or written by the client. Form.io may store more. Types are **Inferred** from usage.

### User (`simflowclientusers`)

| Field | Used for |
|---|---|
| `_id` | Profile PUT, "is this me" check |
| `firstName`, `lastName` | Header, profile, user table, search |
| `email` | Login, profile (read-only), user table, search |
| `password` | Login POST, profile PUT, optional user-admin PUT/POST |
| `userRole.roleName` | Every role check |
| `company._id` | Load company after login |
| `company.data.businessName` | User table and non-admin user filter |
| `isActive` | User table and the admin checkbox |

### Company (`simflowclientdetail`)

| Field | Used for |
|---|---|
| `_id` | Edit modal, user form company select |
| `data.businessName` | Table, search, non-admin filters. Disabled for non-admins in the Form.io editor |
| `data.contactPersonEmail` | Table and search |
| `data.isActive` | Table |
| `data.logoUrl` | Nav logo |
| `data.primaryColor`, `secondaryColor`, `tertiaryColor` | Process card |
| `data.businessProcesses1[]` | Dashboard, Process Hub, task reload |
| `businessProcesses1[].nameOfBusinessProcess` | Card title and modal id (spaces removed) |
| `businessProcesses1[].isTask` | Hub hides tasks; task list keeps them |
| `businessProcesses1[].roleAccess[].roleName` | Visibility |
| `businessProcesses1[].businessProcessForm.formUrl` | Form.io form to render and to list |
| `businessProcesses1[].displayedIcon` | Copied onto submission rows. The card components render `faFileLines`, not this value |

### Submission (shape depends on the process form)

| Field | Used for |
|---|---|
| `_id` | Modal id and Form.io submission URL |
| `data.simflowCardName` | Card title and search. Missing name: dashboard hides the card; task filter can throw |
| `data.simflowCardStatus` | Green vs orange; open-task test is "falsy" |
| `modified` | Recent-list sort |
| `isTask` | Copied from the parent process, not read from Form.io as a submission field |

### Role (`simflowuserroles`)

| Field | Used for |
|---|---|
| `_id` | Select value |
| `data.roleName` | Label and the object sent as `userRole` |

## Constraints

- Submission lists request `limit=500` and have no paging UI. Rows beyond 500 are not shown. Provenance: **Implemented**.
- Management filters for non-admins are query-string equality on business name. Names that need URL encoding are concatenated raw. Provenance: **Implemented**.
- Two Bootstrap majors are loaded (CDN 4.6.2 and npm 5.3.3). Modal behaviour depends on the CDN scripts because the code calls `$().modal` and `data-toggle`. Provenance: **Implemented**.
- The SendGrid API key, when set as a `VITE_` variable, is compiled into the client bundle. Provenance: **Implemented** by the `import.meta.env` read. Whether a key is deployed is **Unknown**.

## Not verified

- A successful login, form render, or FTP deploy.
- Form.io role permissions on the project (the browser will send whatever the JWT allows).
- The contents of `simflowuserroles` and of any company's `businessProcesses1`.
- Pipeline result on `master` after `5952e5d`.
