feat(workflow): add workflow engine module (Phase 4)

Implement complete workflow engine with BPMN subset support:

Backend (erp-workflow crate):
- Token-driven execution engine with exclusive/parallel gateway support
- BPMN parser with flow graph validation
- Expression evaluator for conditional branching
- Process definition CRUD with draft/publish lifecycle
- Process instance management (start, suspend, terminate)
- Task service (pending, complete, delegate)
- PostgreSQL advisory locks for concurrent safety
- 5 database tables: process_definitions, process_instances,
  tokens, tasks, process_variables
- 13 API endpoints with RBAC protection
- Timeout checker framework (placeholder)

Frontend:
- Workflow page with 4 tabs (definitions, pending, completed, monitor)
- React Flow visual process designer (@xyflow/react)
- Process viewer with active node highlighting
- 3 API client modules for workflow endpoints
- Sidebar menu integration
This commit is contained in:
iven
2026-04-11 09:54:02 +08:00
parent 0cbd08eb78
commit 91ecaa3ed7
51 changed files with 4826 additions and 12 deletions

View File

@@ -0,0 +1,89 @@
import client from './client';
import type { PaginatedResponse } from './users';
export interface NodeDef {
id: string;
type: 'StartEvent' | 'EndEvent' | 'UserTask' | 'ServiceTask' | 'ExclusiveGateway' | 'ParallelGateway';
name: string;
assignee_id?: string;
candidate_groups?: string[];
service_type?: string;
position?: { x: number; y: number };
}
export interface EdgeDef {
id: string;
source: string;
target: string;
condition?: string;
label?: string;
}
export interface ProcessDefinitionInfo {
id: string;
name: string;
key: string;
version: number;
category?: string;
description?: string;
nodes: NodeDef[];
edges: EdgeDef[];
status: string;
created_at: string;
updated_at: string;
}
export interface CreateProcessDefinitionRequest {
name: string;
key: string;
category?: string;
description?: string;
nodes: NodeDef[];
edges: EdgeDef[];
}
export interface UpdateProcessDefinitionRequest {
name?: string;
category?: string;
description?: string;
nodes?: NodeDef[];
edges?: EdgeDef[];
}
export async function listProcessDefinitions(page = 1, pageSize = 20) {
const { data } = await client.get<{ success: boolean; data: PaginatedResponse<ProcessDefinitionInfo> }>(
'/workflow/definitions',
{ params: { page, page_size: pageSize } },
);
return data.data;
}
export async function getProcessDefinition(id: string) {
const { data } = await client.get<{ success: boolean; data: ProcessDefinitionInfo }>(
`/workflow/definitions/${id}`,
);
return data.data;
}
export async function createProcessDefinition(req: CreateProcessDefinitionRequest) {
const { data } = await client.post<{ success: boolean; data: ProcessDefinitionInfo }>(
'/workflow/definitions',
req,
);
return data.data;
}
export async function updateProcessDefinition(id: string, req: UpdateProcessDefinitionRequest) {
const { data } = await client.put<{ success: boolean; data: ProcessDefinitionInfo }>(
`/workflow/definitions/${id}`,
req,
);
return data.data;
}
export async function publishProcessDefinition(id: string) {
const { data } = await client.post<{ success: boolean; data: ProcessDefinitionInfo }>(
`/workflow/definitions/${id}/publish`,
);
return data.data;
}

View File

@@ -0,0 +1,65 @@
import client from './client';
import type { PaginatedResponse } from './users';
export interface TokenInfo {
id: string;
node_id: string;
status: string;
created_at: string;
}
export interface ProcessInstanceInfo {
id: string;
definition_id: string;
definition_name?: string;
business_key?: string;
status: string;
started_by: string;
started_at: string;
completed_at?: string;
created_at: string;
active_tokens: TokenInfo[];
}
export interface StartInstanceRequest {
definition_id: string;
business_key?: string;
variables?: Array<{ name: string; var_type?: string; value: unknown }>;
}
export async function startInstance(req: StartInstanceRequest) {
const { data } = await client.post<{ success: boolean; data: ProcessInstanceInfo }>(
'/workflow/instances',
req,
);
return data.data;
}
export async function listInstances(page = 1, pageSize = 20) {
const { data } = await client.get<{ success: boolean; data: PaginatedResponse<ProcessInstanceInfo> }>(
'/workflow/instances',
{ params: { page, page_size: pageSize } },
);
return data.data;
}
export async function getInstance(id: string) {
const { data } = await client.get<{ success: boolean; data: ProcessInstanceInfo }>(
`/workflow/instances/${id}`,
);
return data.data;
}
export async function suspendInstance(id: string) {
const { data } = await client.post<{ success: boolean; data: null }>(
`/workflow/instances/${id}/suspend`,
);
return data.data;
}
export async function terminateInstance(id: string) {
const { data } = await client.post<{ success: boolean; data: null }>(
`/workflow/instances/${id}/terminate`,
);
return data.data;
}

View File

@@ -0,0 +1,61 @@
import client from './client';
import type { PaginatedResponse } from './users';
export interface TaskInfo {
id: string;
instance_id: string;
token_id: string;
node_id: string;
node_name?: string;
assignee_id?: string;
candidate_groups?: unknown;
status: string;
outcome?: string;
form_data?: unknown;
due_date?: string;
completed_at?: string;
created_at: string;
definition_name?: string;
business_key?: string;
}
export interface CompleteTaskRequest {
outcome: string;
form_data?: Record<string, unknown>;
}
export interface DelegateTaskRequest {
delegate_to: string;
}
export async function listPendingTasks(page = 1, pageSize = 20) {
const { data } = await client.get<{ success: boolean; data: PaginatedResponse<TaskInfo> }>(
'/workflow/tasks/pending',
{ params: { page, page_size: pageSize } },
);
return data.data;
}
export async function listCompletedTasks(page = 1, pageSize = 20) {
const { data } = await client.get<{ success: boolean; data: PaginatedResponse<TaskInfo> }>(
'/workflow/tasks/completed',
{ params: { page, page_size: pageSize } },
);
return data.data;
}
export async function completeTask(id: string, req: CompleteTaskRequest) {
const { data } = await client.post<{ success: boolean; data: TaskInfo }>(
`/workflow/tasks/${id}/complete`,
req,
);
return data.data;
}
export async function delegateTask(id: string, req: DelegateTaskRequest) {
const { data } = await client.post<{ success: boolean; data: TaskInfo }>(
`/workflow/tasks/${id}/delegate`,
req,
);
return data.data;
}