37338-vm/WorkflowEngine.php
2026-01-10 17:22:42 +00:00

264 lines
11 KiB
PHP

<?php
// Cache-buster: 1720638682
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['person_id']][$instance['process_definition_id']] = $instance;
}
return [
'people' => $people,
'definitions' => $process_definitions,
'instances' => $instances,
];
}
public function startProcess(string $processCode, int $personId, int $userId): ?int {
$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);
if (!$definition) {
// If no process definition is found, check if there is a definition for a checklist
$stmt_def = $this->pdo->prepare("SELECT * FROM process_definitions WHERE id = ?");
$stmt_def->execute([$processCode]);
$definition = $stmt_def->fetch(PDO::FETCH_ASSOC);
if (!$definition) {
throw new Exception("Process definition with code or id '$processCode' not found.");
}
$definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : [];
if (empty($definition_json) || $definition_json['type'] !== 'checklist') {
throw new Exception("Process definition with code '$processCode' not found or not a checklist.");
}
// For checklists, there's no start_node_id, so we can proceed with instance creation
$startNodeId = null;
} else {
$definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : [];
if (empty($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 (person_id, process_definition_id, current_node_id, current_status, lastActivityAt) VALUES (?, ?, ?, 'in_progress', NOW())"
);
$stmt_insert->execute([$personId, $definition['id'], $startNodeId]);
$instanceId = $this->pdo->lastInsertId();
// 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['process_definition_id']]);
$definition_json = $stmt_def->fetchColumn();
$definition = !empty($definition_json) ? 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;
}
}
public function addNote(int $instanceId, string $message, int $userId): bool {
$state = $this->getProcessState($instanceId);
if (!$state) {
// Even if the instance isn't found, we shouldn't crash. Log and return false.
error_log("addNote failed: Process instance $instanceId not found.");
return false;
}
$currentNodeId = $state['instance']['current_node_id'];
$payload = ['message' => $message];
$this->addEvent($instanceId, 'note', $message, $currentNodeId, $payload, $userId);
return true;
}
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, event_type, message, node_id, payload_json, createdBy, createdAt) VALUES (?, ?, ?, ?, ?, ?, NOW())"
);
$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 `person_id` = ? AND `process_definition_id` = ?");
$stmt->execute([$personId, $processDefinitionId]);
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$instance) {
// For checklists, the process code is the process definition ID
$stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?");
$stmt_def->execute([$processDefinitionId]);
$definition_json = $stmt_def->fetchColumn();
$definition = !empty($definition_json) ? json_decode($definition_json, true) : [];
if ($definition && isset($definition['type']) && $definition['type'] === 'checklist') {
$processCode = (string) $processDefinitionId;
} else {
$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.createdBy = 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;
}
public function getProcessDefinitionNodes(int $processDefinitionId): array {
$stmt = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?");
$stmt->execute([$processDefinitionId]);
$json = $stmt->fetchColumn();
if (!$json) {
return [];
}
$definition = !empty($json) ? json_decode($json, true) : [];
return $definition['nodes'] ?? [];
}
}