# Workflow Engine

The Workflow Engine executes automated steps when specific events occur. Each workflow record is stored in the `workflows` table with an associated list of steps in `workflow_steps`.

A workflow belongs to a `tenant_id` and may be limited to a `department_id`. The `trigger_event` column defines the event name that starts the workflow.

When an event is emitted the engine searches for workflows in the following order:
1. Department level – rows matching both the current `tenant_id` and `department_id`.
2. Tenant level – rows with the current `tenant_id` and a `NULL` department.
3. Global – workflows where both identifiers are `NULL`.
This precedence ensures department specific rules run before broader tenant or global workflows.
See [`HierarchicalConfiguration.md`](HierarchicalConfiguration.md) for a summary of how this hierarchy works across modules.

Workflow queries bypass the automatic tenant filters so global workflow and step
definitions are always returned. This ensures that department, tenant and
globally scoped workflows remain available regardless of the current tenant
settings.

## Configuring Workflows

Use the REST endpoints under `/api/workflows` to manage definitions.

```json
{
  "name": "Employee Onboarding",
  "trigger_event": "employee.hired",
  "is_active": 1,
  "tenant_id": 1,
  "department_id": null,
  "steps": [
    { "type": "assign_items" },
    { "type": "notify_manager" },
    { "type": "send_welcome" }
  ]
}
```

Posting this JSON creates a workflow and inserts the steps in order. Updates replace the entire step list. Activate or deactivate a workflow with `PUT /api/workflows/{id}/activate` and a JSON body of `{ "is_active": 1 }` or `0`.

## Triggering Events

Modules fire events such as `employee.hired` or `incident.reported` through the `WorkflowEngine` service. When an event is emitted, the engine loads active workflows with a matching `trigger_event` and executes each step sequentially. Step handlers may send emails, update records or wait for approvals depending on the `step_type` and configuration.

The attendance and payroll modules emit additional events:

- `timeclock.created` – fired after a time clock entry is created.
- `timesheet.approved` – fired whenever a timesheet is approved.
- `timesheet.rejected` – fired whenever a timesheet is rejected.
- `case_hours.approved` – fired after a case hour entry is approved. Default
  workflows update the record status to **Approved**, store any comments and
  queue a notification.
- `case_hours.rejected` – fired when a case hour entry is rejected. Default
  workflows mark the entry **Rejected**, save the supplied comments and queue
  a notification.

The fleet module provides these triggers:

- `vehicle.registered` – after a vehicle record is created.
- `vehicle.reported` – when a maintenance or issue report is submitted.
- `trip.started` – when a trip log begins.
- `trip.ended` – when a trip record is closed.
- `service.due` – when a vehicle's service date is reached.

### Form Events

Form submissions emit events when a record changes:

- `form.{form_id}.updated` – fired after a submission is updated.
- `form.{form_id}.deleted` – fired when a submission is deleted.

Define workflows using these trigger names to automate notifications or other follow up tasks.

### UI Action Triggers

Buttons may also fire events via the `ui_actions` table. If a row defines a `workflow_event`, the client posts to `/api/ui_actions/trigger/{button_id}` when the matching button is clicked. The controller then calls `WorkflowEngine->emit()` with that event name to start the workflow.

`sql/defaults.sql` seeds one `ui_actions` row for each dashboard card slug with `workflow_event` set to `dashboard.card.permissions`. These defaults enable the right-click context menu on the dashboard to open the permissions modal without any additional configuration.

Definitions are cached using `CacheService` to reduce database lookups. Clearing the cache or updating a workflow immediately invalidates the cached entry.

### Custom Step Classes

Workflow step handlers live under `api/services/workflow_steps`.  When `WorkflowEngine` executes a step it converts the `type` to a class name such as `EmailStep` or `OpenModalStep`.  If a matching file exists the class is loaded and its `execute()` method is invoked.  Adding a new step therefore only requires creating a class that implements `WorkflowStepInterface`; no changes to `WorkflowEngine.php` are needed.

### Database Event Triggers

The `workflow_event_triggers` table links table operations to workflow events. Each row defines
an `event_name`, the `table_name` to monitor and a `trigger_type` of `insert`, `update` or
`delete`. Optional `form_id` limits the trigger to a specific form submission table. The
`operations` column can contain a JSON array of operations (`"insert"`, `"update"`, `"delete"`)
to match. The `field_map` column stores JSON mapping database columns to keys in the workflow
context. After a matching operation occurs the system loads the mapping, builds the context and
calls `WorkflowEngine->emit()` with the configured event name.

Example `field_map` value:

```json
{ "record_id": "id", "employee_id": "user_id" }
```

With this mapping inserting into `time_clocks` would emit the context
`{"record_id": 5, "employee_id": 7}`.

After modifying workflow tables in `TAFDB.sql` run `./test_setup.sh` to recreate the schema.

### API Webhook Triggers

Workflows may also be started by an external service via a unique token. Each
token is stored in the `workflow_api_triggers` table along with the workflow to
run and the allowed HTTP method. To invoke a workflow send a request to
`/api/webhook/{token}` using the configured method. Any JSON body is passed to
`WorkflowEngine->emit()` as the context.

Example:

```bash
curl -X POST https://example.com/api/webhook/abc123 \
     -H 'Content-Type: application/json' \
     -d '{"id":5}'
```

This triggers the workflow linked to `abc123` with `{"id":5}` as the context.

### Additional Workflow Triggers

Workflows can listen for more than one event. The `workflow_triggers` table
stores extra `event_name` values associated with a `workflow_id`. When an
event is emitted, `WorkflowEngine` loads workflows whose `trigger_event`
matches **or** which have a corresponding row in `workflow_triggers`. This table
is mainly used by the Workflow Builder when the start step is configured with
multiple forms. Each selected form inserts a row like `form.{id}.updated` so the
workflow runs whenever any of those forms are submitted.

## Approval Storage

The `workflow_approvals` table holds pending approval records created when a `wait_approval` step executes.

| Column | Description |
| ------ | ----------- |
| `id` | Primary key |
| `workflow_id` | Related workflow |
| `step_order` | Step number awaiting approval |
| `approver_role` | Role allowed to approve |
| `status` | `pending`, `approved` or `rejected` |
| `record_id` | ID of the affected record |

To update an approval call `PUT /api/workflow_approvals/{id}` with a JSON body like:

```json
{ "status": "approved", "context": { "email": "user@example.com" } }
```

When the status becomes `approved`, the `WorkflowEngine` continues executing the remaining steps for that workflow.

## Workflow States and Transitions

Branching workflows use two additional tables:

```
workflow_states(id, workflow_id, name, state_order, isDeleted, updatedAt)
workflow_transitions(id, workflow_id, from_state_id, to_state_id,
                     action, conditions, transition_order, isDeleted, updatedAt)
```

States define the possible positions of a workflow while transitions move from
one state to another based on a user action or set of conditions. Conditions are
stored as JSON using the same format as workflow level conditions.

### Example Usage

```php
$wf = new \App\Services\WorkflowEngine();
$ctx = [];
$wf->startWorkflow(1, $ctx);          // sets $ctx['state_id'] to first state
$wf->advanceWorkflow(1, 'approve', $ctx); // moves to the "approved" state
```

A POST to `/api/workflows` can include `states` and `transitions` arrays to
create the state machine:

```json
{
  "name": "Doc Approval",
  "trigger_event": "doc.submit",
  "states": [
    {"name": "pending"},
    {"name": "approved"},
    {"name": "rejected"}
  ],
  "transitions": [
    {"from": 1, "to": 2, "action": "approve"},
    {"from": 1, "to": 3, "action": "reject"}
  ]
}
```

## Step Assignments

The `assign_items` step inserts rows into `employee_items` to track laptops,
badges or other equipment issued during onboarding. Each item may specify an
`assignee_user_id` or `assignee_group_id` so follow‑up tasks are assigned to the
correct person or team.

Example step:

```json
{
  "type": "assign_items",
  "custom_items": [
    { "name": "Laptop", "assignee_user_id": 5 },
    { "name": "ID Badge", "assignee_group_id": 2 }
  ]
}
```

## Create Task

The `create_task` step adds a row to the `kanban_tasks` table. Values may come
from the workflow context or the step configuration. At minimum provide a
`column_id` and `title`. Additional fields like `assignee_id` and `due_date` are
optional.

Example step:

```json
{
  "type": "create_task",
  "title": "Kickoff {project}",
  "column_id": 3,
  "assignee_id": 5
}
```

## Add Calendar Event

Use the `add_calendar_event` step to schedule meetings or reminders.
Provide a `title` and `start_time` in `YYYY-MM-DD HH:MM:SS` format.  An optional
`end_time` may be included; otherwise the start time is used for both fields.

Example step:

```json
{
  "type": "add_calendar_event",
  "title": "Kickoff Meeting",
  "start_time": "2025-01-01 09:00:00",
  "end_time": "2025-01-01 10:00:00"
}
```

## Create Leave Request

The `create_leave_request` step logs a leave entry for each employee in the
workflow context. Pass `employee_id` for a single user or `employee_ids` for
multiple workers. Optional `start_date` and `end_date` fields control the date
range.

Example step:

```json
{
  "type": "create_leave_request",
  "start_date": "2024-05-01",
  "end_date": "2024-05-03"
}
```

## Update Attendance

The `update_attendance` step modifies records in the `attendance` table. Use it
to apply restricted duty hours or mark employees absent across several days.
Provide the same `employee_id` or `employee_ids` context plus a date range. When
`restricted_start` and `restricted_end` are supplied the engine updates those
columns; setting `absence` to `true` writes zero hours worked.

Example step:

```json
{
  "type": "update_attendance",
  "start_date": "2024-05-01",
  "end_date": "2024-05-03",
  "restricted_start": "09:00:00",
  "restricted_end": "13:00:00"
}
```

## Open Modal

The `open_modal` step queues a command instructing the browser to display a
predefined modal using `ModalBuilder`. When the step executes it inserts a row
into `notification_queue` with a payload like:

```json
{ "command": { "action": "open_modal", "name": "My Modal", "context": {...} } }
```

The PostgreSQL trigger on that table immediately publishes the payload to the
`taf_queue` channel which `websocket-server/pgListener.js` relays to Redis. Any
connected WebSocket clients then receive the command in real time. The modal
name comes from the step's `modal` property and the payload includes the
workflow context.

Rows queued with `event_type: command` are published by `ProcessNotificationQueue`
with the command at the top level of the Redis message. The WebSocket server
detects this and forwards the command object directly to subscribed clients
without converting it into an `updates` payload.

Example step:

```json
{
  "type": "open_modal",
  "modal": "confirm_complete"
}
```

## Schedule Services

The `schedule_services` step creates upcoming maintenance records for a newly
registered vehicle. Service types whose `make`, `model` or `type` match the
vehicle (or are `NULL`) are selected. For each match a row is inserted into
`vehicle_services` with the next due date calculated from `month_interval` and
`km_interval`. Set `create_task` or `create_event` to also generate a kanban task
or calendar event for the due date.

Example step:

```json
{
  "type": "schedule_services",
  "create_task": true,
  "task_column_id": 4,
  "create_event": true
}
```

## Create Procurement Request

Procurement requests are now built from generic steps. Start with
`query_budget` to pull the remaining amount for the desired category:

```json
{ "type": "query_budget", "category": "{category}" }
```

Next a `condition` step compares the returned `budget_amount` against the
request's `estimated_cost`:

```json
{
  "type": "condition",
  "conditions": [
    { "field": "budget_amount", "operator": ">=", "value": "{estimated_cost}" }
  ]
}
```

When the condition passes add a `save_record` step configured for the
`procurement_requests` table. Existing `wait_approval` and `alert` steps can
follow to complete the process.

## Query Records

Use the `query_records` step to load rows from any table. Specify the table name
and optional `where` clauses. Matching rows are stored under the configured
`context_key` (defaults to `records`).

```json
{
  "type": "query_records",
  "table": "alerts",
  "where": { "status": "open" },
  "context_key": "alerts"
}
```

## For Each Record

The `for_each_record` step iterates over an array of rows in the workflow
context. Provide the array name via `list_key` and a sequence of `steps` to run
for each row.

```json
{
  "type": "for_each_record",
  "list_key": "alerts",
  "steps": [
    { "type": "email", "to": "{email}", "message": "Alert {id} requires action" }
  ]
}
```

In this example `query_records` loads open alerts and `for_each_record` sends an
email notification for each one.

## REST API Endpoints

Workflows and approvals use standard REST routes.

```
GET    /api/workflows
GET    /api/workflows/{id}
POST   /api/workflows
PUT    /api/workflows/{id}
DELETE /api/workflows/{id}
PUT    /api/workflows/{id}/activate
GET    /api/workflow_approvals
GET    /api/workflow_approvals/{id}
PUT    /api/workflow_approvals/{id}
```

Creating or updating a workflow may include `steps`, `states` and `transitions`
as shown above.

## Admin Interface

The page `FrontEnd/workflow-admin.html` provides a simple UI for managing
workflows. It lists existing definitions in a table and opens a modal dialog to
edit the name, trigger event, active flag and JSON definition. The accompanying
script `FrontEnd/js/workflows.js` handles loading records from
`/api/workflows` and posting changes back to the same endpoints.
Step options are populated from the `workflow_step_types` table so the palette
includes entries like `create_task` and `add_calendar_event`. Selecting the
`add_calendar_event` type lets administrators schedule calendar items directly
from a workflow definition.

## Runtime Tables

The engine stores progress in `workflow_instances` and `workflow_tasks`.

### `workflow_instances`

| Column | Description |
| ------ | ----------- |
| `id` | Primary key |
| `workflow_id` | Definition being executed |
| `record_id` | ID of the record that triggered the workflow |
| `current_state_id` | Pointer to the active state |
| `started_by` | User who started the instance |
| `status` | `active` or `complete` |
| `started_at` | When execution began |
| `isDeleted` | Soft delete flag |
| `updatedAt` | Last update timestamp |

### `workflow_tasks`

Each state generates a task assigned to a user or role.

| Column | Description |
| ------ | ----------- |
| `id` | Primary key |
| `instance_id` | Related workflow instance |
| `state_id` | State awaiting action |
| `assignee_user_id` | Specific user to perform the task |
| `assignee_role` | Role that can complete the task |
| `status` | `pending` or `complete` |
| `due_at` | Optional due date |
| `isDeleted` | Soft delete flag |
| `updatedAt` | Last update timestamp |

## Starting and Acting on an Instance

A workflow is launched with `POST /api/workflow/start/{id}`.

```bash
curl -X POST /api/workflow/start/5 -d '{"record_id":42}'
```

This returns the new instance ID. To advance it call
`POST /api/workflow/instance/{id}/action`:

```bash
curl -X POST /api/workflow/instance/1/action -d '{"action":"approve"}'
```

## Task Assignment and Completion

When a state with an assigned user or role becomes active the engine
creates a row in `workflow_tasks`. These appear in the user's **My
Workflow Tasks** module (see `FrontEnd/JsLibs2/modules/WorkflowTasks.js`).
Posting an action completes the current task and creates the next one
until the workflow
finishes.

## Example Workflows

### Training Feedback

Triggered when an employee completes a session.

```json
{
  "name": "Training Feedback",
  "trigger_event": "training.completed",
  "steps": [
    { "type": "request_feedback", "feedback_type": "peer" }
  ]
}
```

### Goal Milestone

Fires whenever a goal milestone is updated.

```json
{
  "name": "Goal Milestone Reached",
  "trigger_event": "goal.milestone",
  "steps": [
    { "type": "request_feedback", "feedback_type": "manager" }
  ]
}
```

### Workplace Registration Review

Triggered when a workplace registration form is submitted. The workflow
notifies the assigned reviewer and thanks the registrant before generating
required documents.

```json
{
  "name": "Workplace Registration Review",
  "trigger_event": "workplace.registration.created",
  "steps": [
    { "type": "assign_reviewer" },
    { "type": "email", "to": "{reviewer_email}", "message": "A new workplace registration requires review." },
    { "type": "save_record" },
    { "type": "email", "to": "{contact_email}", "message": "Thank you for registering your workplace." },
    { "type": "create_certificate" },
    { "type": "generate_certificate_document" },
    { "type": "create_invoice" },
    { "type": "open_modal" },
    { "type": "open_modal" }
  ]
}
```

## Debugging

Set the `DEBUG_MODE` constant in `api/config.php` to `true` to enable verbose
logging. When active, the engine writes step execution details to
`logs/workflow_debug.log`. Each step's start, completion and any thrown
exceptions are appended to this file, and `audit_logs` entries are skipped.
