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; } }