216 lines
8.5 KiB
PHP
216 lines
8.5 KiB
PHP
<?php
|
|
require_once __DIR__ . '/db/config.php';
|
|
|
|
class WorkflowEngine {
|
|
|
|
private $pdo;
|
|
|
|
public function __construct() {
|
|
$this->pdo = db();
|
|
}
|
|
|
|
public function getDashboardMatrix(): array {
|
|
// Get all people (potential assignees)
|
|
$stmt_people = $this->pdo->prepare("SELECT id, firstName, lastName, companyName, role, email, phone FROM people ORDER BY lastName, firstName");
|
|
$stmt_people->execute();
|
|
$people = $stmt_people->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Fetch all process definitions
|
|
$stmt_defs = $this->pdo->prepare("SELECT id, name FROM process_definitions ORDER BY name");
|
|
$stmt_defs->execute();
|
|
$process_definitions = $stmt_defs->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Fetch instances
|
|
$stmt_instances = $this->pdo->prepare("SELECT * FROM process_instances");
|
|
$stmt_instances->execute();
|
|
$instances_data = $stmt_instances->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$instances = [];
|
|
foreach ($instances_data as $instance) {
|
|
$instances[$instance['personId']][$instance['processDefinitionId']] = $instance;
|
|
}
|
|
|
|
return [
|
|
'people' => $people,
|
|
'definitions' => $process_definitions,
|
|
'instances' => $instances,
|
|
];
|
|
}
|
|
|
|
public function startProcess(string $processCode, int $personId, int $userId): ?int {
|
|
error_log("startProcess: processCode=$processCode, personId=$personId, userId=$userId");
|
|
$this->pdo->beginTransaction();
|
|
try {
|
|
// 1. Find active process definition by code.
|
|
$stmt_def = $this->pdo->prepare("SELECT * FROM process_definitions WHERE code = ? AND active = 1");
|
|
$stmt_def->execute([$processCode]);
|
|
$definition = $stmt_def->fetch(PDO::FETCH_ASSOC);
|
|
error_log("startProcess: definition=" . print_r($definition, true));
|
|
|
|
if (!$definition) {
|
|
throw new Exception("Active process definition with code '$processCode' not found.");
|
|
}
|
|
|
|
$definition_json = json_decode($definition['definition_json'], true);
|
|
if (!$definition_json || !isset($definition_json['start_node_id'])) {
|
|
throw new Exception("Process definition is missing start_node_id.");
|
|
}
|
|
$startNodeId = $definition_json['start_node_id'];
|
|
|
|
// 2. Create a new process instance.
|
|
$stmt_insert = $this->pdo->prepare(
|
|
"INSERT INTO process_instances (personId, processDefinitionId, current_node_id, current_status, lastActivityAt) VALUES (?, ?, ?, 'in_progress', NOW())"
|
|
);
|
|
$stmt_insert->execute([$personId, $definition['id'], $startNodeId]);
|
|
error_log("startProcess: affected rows=" . $stmt_insert->rowCount());
|
|
$instanceId = $this->pdo->lastInsertId();
|
|
error_log("startProcess: instanceId=$instanceId");
|
|
|
|
// 3. Create a system event for process start.
|
|
$this->addEvent($instanceId, 'system', 'Process started.', $startNodeId, [], $userId);
|
|
|
|
$this->pdo->commit();
|
|
return (int)$instanceId;
|
|
} catch (Exception $e) {
|
|
$this->pdo->rollBack();
|
|
error_log("Error in startProcess: " . $e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public function getProcessState(int $instanceId): ?array {
|
|
$stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE id = ?");
|
|
$stmt->execute([$instanceId]);
|
|
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$instance) {
|
|
return null;
|
|
}
|
|
|
|
$stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?");
|
|
$stmt_def->execute([$instance['processDefinitionId']]);
|
|
$definition_json = $stmt_def->fetchColumn();
|
|
$definition = json_decode($definition_json, true);
|
|
|
|
$currentNodeId = $instance['current_node_id'];
|
|
$nodeInfo = $definition['nodes'][$currentNodeId] ?? null;
|
|
|
|
return [
|
|
'instance' => $instance,
|
|
'definition' => $definition,
|
|
'currentNode' => $nodeInfo,
|
|
];
|
|
}
|
|
|
|
public function applyTransition(int $instanceId, string $transitionId, array $inputPayload, int $userId): bool {
|
|
$this->pdo->beginTransaction();
|
|
try {
|
|
$state = $this->getProcessState($instanceId);
|
|
if (!$state) {
|
|
throw new Exception("Process instance not found.");
|
|
}
|
|
|
|
$instance = $state['instance'];
|
|
$definition = $state['definition'];
|
|
$currentNodeId = $instance['current_node_id'];
|
|
|
|
// Find the transition from the definition
|
|
$transition = null;
|
|
foreach ($definition['transitions'] as $t) {
|
|
if ($t['from'] === $currentNodeId && $t['id'] === $transitionId) {
|
|
$transition = $t;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$transition) {
|
|
throw new Exception("Transition not found or not allowed from the current node.");
|
|
}
|
|
|
|
// TODO: Add rule validation here
|
|
|
|
$newNodeId = $transition['to'];
|
|
$newNodeInfo = $definition['nodes'][$newNodeId] ?? null;
|
|
|
|
// Update instance
|
|
$stmt_update = $this->pdo->prepare(
|
|
"UPDATE process_instances SET current_node_id = ?, current_status = ?, current_reason = ?, suggested_next_step = ?, lastActivityAt = NOW() WHERE id = ?"
|
|
);
|
|
$stmt_update->execute([
|
|
$newNodeId,
|
|
$newNodeInfo['ui_hints']['status'] ?? 'in_progress',
|
|
$newNodeInfo['ui_hints']['reason'] ?? '',
|
|
$newNodeInfo['ui_hints']['next_step'] ?? '',
|
|
$instanceId
|
|
]);
|
|
|
|
// Add event
|
|
$message = $inputPayload['message'] ?? $transition['name'];
|
|
$this->addEvent($instanceId, 'transition_applied', $message, $newNodeId, $inputPayload, $userId);
|
|
|
|
$this->pdo->commit();
|
|
return true;
|
|
} catch (Exception $e) {
|
|
$this->pdo->rollBack();
|
|
error_log("Error in applyTransition: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private function addEvent(int $instanceId, string $eventType, string $message, ?string $nodeId, array $payload, int $userId): void {
|
|
$stmt = $this->pdo->prepare(
|
|
"INSERT INTO process_events (processInstanceId, eventType, message, node_id, payload_json, createdById) VALUES (?, ?, ?, ?, ?, ?)"
|
|
);
|
|
$stmt->execute([$instanceId, $eventType, $message, $nodeId, json_encode($payload), $userId]);
|
|
}
|
|
|
|
public function getOrCreateInstanceByDefId(int $personId, int $processDefinitionId, int $userId): ?array {
|
|
$stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE personId = ? AND processDefinitionId = ?");
|
|
$stmt->execute([$personId, $processDefinitionId]);
|
|
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$instance) {
|
|
$stmt_def = $this->pdo->prepare("SELECT code FROM process_definitions WHERE id = ?");
|
|
$stmt_def->execute([$processDefinitionId]);
|
|
$processCode = $stmt_def->fetchColumn();
|
|
|
|
if($processCode) {
|
|
$instanceId = $this->startProcess($processCode, $personId, $userId);
|
|
if($instanceId) {
|
|
$stmt->execute([$personId, $processDefinitionId]);
|
|
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $instance !== false ? $instance : null;
|
|
}
|
|
|
|
public function getEvents(int $instanceId): array {
|
|
$stmt_events = $this->pdo->prepare("SELECT pe.*, p.email as user_email, p.firstName, p.lastName FROM process_events pe JOIN people p ON pe.createdById = p.id WHERE pe.processInstanceId = ? ORDER BY pe.createdAt DESC");
|
|
$stmt_events->execute([$instanceId]);
|
|
return $stmt_events->fetchAll(PDO::FETCH_ASSOC);
|
|
}
|
|
|
|
public function getAvailableTransitions(int $instanceId): array {
|
|
$state = $this->getProcessState($instanceId);
|
|
if (!$state) {
|
|
return [];
|
|
}
|
|
|
|
$currentNodeId = $state['instance']['current_node_id'];
|
|
$definition = $state['definition'];
|
|
|
|
$transitions = [];
|
|
if (isset($definition['transitions'])) {
|
|
foreach ($definition['transitions'] as $t) {
|
|
if ($t['from'] === $currentNodeId) {
|
|
$transitions[] = $t;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $transitions;
|
|
}
|
|
}
|