pdo = db(); } public function getDashboardMatrix(?string $searchTerm = null, ?int $groupId = null, ?int $activeProcessDefinitionId = null): array { // 1. Base query for people $sql_people = "SELECT p.*, bg.name as bni_group_name FROM people p LEFT JOIN bni_groups bg ON p.bni_group_id = bg.id"; $params = []; $where_clauses = []; // 2. Add filter conditions if ($searchTerm) { $where_clauses[] = "(p.first_name LIKE :search OR p.last_name LIKE :search OR p.company_name LIKE :search OR p.email LIKE :search)"; $params[':search'] = '%' . $searchTerm . '%'; } if ($groupId) { $where_clauses[] = "p.bni_group_id = :group_id"; $params[':group_id'] = $groupId; } if ($activeProcessDefinitionId) { $terminal_statuses = ['positive', 'negative', 'completed', 'error', 'inactive']; $in_clause = implode(',', array_map([$this->pdo, 'quote'], $terminal_statuses)); $sql_people .= " INNER JOIN process_instances pi ON p.id = pi.person_id"; $where_clauses[] = "pi.process_definition_id = :active_process_id AND (pi.current_status IS NOT NULL AND pi.current_status NOT IN ($in_clause))"; $params[':active_process_id'] = $activeProcessDefinitionId; } if (!empty($where_clauses)) { $sql_people .= " WHERE " . implode(" AND ", $where_clauses); } $sql_people .= " ORDER BY p.last_name, p.first_name"; // 3. Execute query to get filtered people $stmt_people = $this->pdo->prepare($sql_people); $stmt_people->execute($params); $people = $stmt_people->fetchAll(PDO::FETCH_ASSOC); // 4. Fetch all process definitions with their JSON $stmt_defs = $this->pdo->prepare("SELECT id, name, definition_json, is_active FROM process_definitions WHERE is_active = 1 ORDER BY sort_order, name"); $stmt_defs->execute(); $process_definitions_raw = $stmt_defs->fetchAll(PDO::FETCH_ASSOC); $definitions = []; $definition_map = []; foreach ($process_definitions_raw as $def) { $definitions[$def['id']] = [ 'id' => $def['id'], 'name' => $def['name'], 'is_active' => $def['is_active'] ]; $definition_map[$def['id']] = !empty($def['definition_json']) ? json_decode($def['definition_json'], true) : null; } // 5. Fetch instances ONLY for the filtered people $instances = []; $person_ids = array_column($people, 'id'); if (!empty($person_ids)) { $placeholders = implode(',', array_fill(0, count($person_ids), '?')); $stmt_instances = $this->pdo->prepare("SELECT * FROM process_instances WHERE person_id IN ($placeholders)"); $stmt_instances->execute($person_ids); $instances_data = $stmt_instances->fetchAll(PDO::FETCH_ASSOC); foreach ($instances_data as $instance) { $enriched_instance = $instance; $def_id = $instance['process_definition_id']; $node_id = $instance['current_node_id']; $definition = $definition_map[$def_id] ?? null; if ($definition && isset($definition['type']) && $definition['type'] === 'checklist') { $tasks = $definition['tasks'] ?? []; $instanceData = $instance['data_json'] ? json_decode($instance['data_json'], true) : []; $totalTasks = count($tasks); $completedTasks = 0; if(is_array($instanceData)) { foreach ($tasks as $task) { if (!empty($instanceData[$task['code']])) { $completedTasks++; } } } if ($totalTasks > 0 && $completedTasks === $totalTasks) { $status = 'completed'; } elseif ($completedTasks > 0) { $status = 'in_progress'; } else { $status = 'inactive'; } $enriched_instance['computed_status'] = $status; $enriched_instance['computed_reason'] = "$completedTasks/$totalTasks completed"; $enriched_instance['computed_next_step'] = ''; } else if ($definition && isset($definition['nodes'][$node_id])) { $node_info = $definition['nodes'][$node_id]; $enriched_instance['computed_status'] = $node_info['ui_hints']['status'] ?? $instance['current_status']; $enriched_instance['computed_reason'] = $node_info['ui_hints']['reason'] ?? $instance['current_reason']; $enriched_instance['computed_next_step'] = $node_info['ui_hints']['next_step'] ?? $instance['suggested_next_step']; } else { $enriched_instance['computed_status'] = $instance['current_status']; $enriched_instance['computed_reason'] = $instance['current_reason']; $enriched_instance['computed_next_step'] = $instance['suggested_next_step']; } $instances[$instance['person_id']][$def_id] = $enriched_instance; } } // 6. Fetch ancillary data $stmt_functions = $this->pdo->query("SELECT * FROM functions ORDER BY display_order"); $all_functions = $stmt_functions->fetchAll(PDO::FETCH_ASSOC); $stmt_person_functions = $this->pdo->query("SELECT user_id, function_id FROM user_functions"); $person_functions_map = []; while ($row = $stmt_person_functions->fetch(PDO::FETCH_ASSOC)) { $person_functions_map[$row['user_id']][] = $row['function_id']; } $stmt_bni_groups = $this->pdo->query("SELECT * FROM bni_groups ORDER BY name"); $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC); // 7. Fetch Spotkania columns (upcoming meetings) $today = date('Y-m-d H:i:s'); $stmt_meetings = $this->pdo->prepare(" WITH RankedMeetings AS ( SELECT bg.id as group_id, bg.name as group_name, ce.start_datetime, ROW_NUMBER() OVER(PARTITION BY bg.id ORDER BY ce.start_datetime) as rn FROM bni_groups bg JOIN calendar_event_groups ceg ON bg.id = ceg.bni_group_id JOIN calendar_events ce ON ceg.calendar_event_id = ce.id WHERE ce.start_datetime >= :today ) SELECT group_id, group_name, start_datetime FROM RankedMeetings WHERE rn <= 3 ORDER BY group_id, start_datetime; "); $stmt_meetings->execute(['today' => $today]); $upcoming_meetings_flat = $stmt_meetings->fetchAll(PDO::FETCH_ASSOC); $spotkania_cols = []; foreach ($upcoming_meetings_flat as $meeting) { $spotkania_cols[$meeting['group_id']]['group_id'] = $meeting['group_id']; $spotkania_cols[$meeting['group_id']]['group_name'] = $meeting['group_name']; $spotkania_cols[$meeting['group_id']]['meetings'][] = $meeting['start_datetime']; } return [ 'people' => $people, 'definitions' => array_values($definitions), 'instances' => $instances, 'all_functions' => $all_functions, 'person_functions_map' => $person_functions_map, 'bni_groups' => $bni_groups, 'spotkania_cols' => $spotkania_cols, // Add this to the return array ]; } 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 is_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 WorkflowNotFoundException("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 WorkflowNotAllowedException("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 WorkflowRuleFailedException("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, last_activity_at) 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(); throw $e; } } 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): array { $this->pdo->beginTransaction(); try { $state = $this->getProcessState($instanceId); if (!$state) { throw new WorkflowNotFoundException("Process instance not found."); } $instance = $state['instance']; $definition = $state['definition']; $currentNodeId = $instance['current_node_id']; $transition = null; foreach ($definition['transitions'] as $t) { if ($t['from'] === $currentNodeId && $t['id'] === $transitionId) { // For user-triggered transitions, we ignore the condition here. // The UI should prevent showing buttons for transitions whose data-based conditions aren't met. $transition = $t; break; } } if (!$transition) { throw new WorkflowNotAllowedException("Transition not found or not allowed from the current node."); } // Apply the initial, user-triggered transition $newNodeId = $this->applySingleTransition($instanceId, $instance, $definition, $transition, $inputPayload, $userId); $instance['current_node_id'] = $newNodeId; // Update instance state for the loop // Loop for automatic transitions (router nodes) for ($i = 0; $i < 10; $i++) { // Max 10 auto-steps to prevent infinite loops $autoTransition = $this->findAutomaticTransition($instance, $definition); if ($autoTransition) { // Automatic transitions have no user payload $newNodeId = $this->applySingleTransition($instanceId, $instance, $definition, $autoTransition, [], $userId); $instance['current_node_id'] = $newNodeId; // Update for next iteration } else { break; // No more automatic transitions found } } $this->pdo->commit(); // Refetch the final state of the instance to return the correct status $finalState = $this->getProcessState($instanceId)['instance']; $finalNodeInfo = $definition['nodes'][$finalState['current_node_id']] ?? null; return [ 'instanceId' => $instanceId, 'currentNodeId' => $finalState['current_node_id'], 'currentStatus' => $finalNodeInfo['ui_hints']['status'] ?? $finalState['current_status'], 'currentReason' => $finalNodeInfo['ui_hints']['reason'] ?? $finalState['current_reason'], 'suggestedNextStep' => $finalNodeInfo['ui_hints']['next_step'] ?? $finalState['suggested_next_step'], 'lastActivityAt' => $finalState['last_activity_at'], ]; } catch (Exception $e) { $this->pdo->rollBack(); throw $e; } } private function applySingleTransition(int $instanceId, array &$instance, array $definition, array $transition, array $inputPayload, int $userId): string { $newNodeId = $transition['to']; $newNodeInfo = $definition['nodes'][$newNodeId] ?? null; $newStatus = $newNodeInfo['ui_hints']['status'] ?? 'in_progress'; $newReason = $newNodeInfo['ui_hints']['reason'] ?? ''; $newNextStep = $newNodeInfo['ui_hints']['next_step'] ?? ''; $stmt_update = $this->pdo->prepare( "UPDATE process_instances SET current_node_id = ?, current_status = ?, current_reason = ?, suggested_next_step = ?, last_activity_at = NOW() WHERE id = ?" ); $stmt_update->execute([ $newNodeId, $newStatus, $newReason, $newNextStep, $instanceId ]); $message = $inputPayload['message'] ?? $transition['name']; $this->addEvent($instanceId, 'transition_applied', $message, $newNodeId, $inputPayload, $userId); if (isset($transition['actions'])) { foreach ($transition['actions'] as $action) { if ($action['type'] === 'start_process') { $this->executeStartProcessAction($instance['person_id'], $action, $userId); } elseif ($action['type'] === 'set_data') { // Pass the instance by reference to be updated with new data $this->executeSetDataAction($instanceId, $instance, $action, $inputPayload); } } } return $newNodeId; } private function findAutomaticTransition(array $instance, array $definition): ?array { $currentNodeId = $instance['current_node_id']; foreach ($definition['transitions'] as $transition) { if ($transition['from'] === $currentNodeId) { // An automatic transition MUST have a condition. if (isset($transition['condition'])) { if ($this->checkTransitionCondition($transition, $instance)) { return $transition; } } } } return null; } public function addNote(int $instanceId, string $message, int $userId): bool { $state = $this->getProcessState($instanceId); if (!$state) { throw new WorkflowNotFoundException("Process instance #$instanceId not found."); } $currentNodeId = $state['instance']['current_node_id']; $payload = ['message' => $message]; $this->addEvent($instanceId, 'note', $message, $currentNodeId, $payload, $userId); return true; } public function applyManualStatus(int $instanceId, string $status, string $reasonOrNote, int $userId): bool { $this->pdo->beginTransaction(); try { $state = $this->getProcessState($instanceId); if (!$state) { throw new WorkflowNotFoundException("Process instance #$instanceId not found."); } $stmt_update = $this->pdo->prepare( "UPDATE process_instances SET current_status = ?, current_reason = ?, last_activity_at = NOW() WHERE id = ?" ); $stmt_update->execute([$status, $reasonOrNote, $instanceId]); $currentNodeId = $state['instance']['current_node_id']; $message = "Status manually set to '$status'."; if (!empty($reasonOrNote)) { $message .= " Reason: $reasonOrNote"; } $this->addEvent($instanceId, 'manual_status_change', $message, $currentNodeId, ['status' => $status, 'reason' => $reasonOrNote], $userId); $this->pdo->commit(); return true; } catch (Exception $e) { $this->pdo->rollBack(); throw $e; } } public function bulkAddNotes(array $notes): array { $results = []; foreach ($notes as $note) { try { $this->addNote((int)$note['instance_id'], $note['message'], (int)$note['user_id']); $results[] = ['instance_id' => $note['instance_id'], 'success' => true]; } catch (Exception $e) { $results[] = ['instance_id' => $note['instance_id'], 'success' => false, 'error' => $e->getMessage()]; } } return $results; } public function bulkManualStatus(array $statuses): array { $results = []; foreach ($statuses as $status) { try { $this->applyManualStatus((int)$status['instance_id'], $status['status'], $status['reason'] ?? '', (int)$status['user_id']); $results[] = ['instance_id' => $status['instance_id'], 'success' => true]; } catch (Exception $e) { $results[] = ['instance_id' => $status['instance_id'], 'success' => false, 'error' => $e->getMessage()]; } } return $results; } public function updateChecklistStatus(int $instanceId, string $taskCode, bool $isChecked, int $userId): array { $this->pdo->beginTransaction(); try { // Get current data_json $stmt = $this->pdo->prepare("SELECT data_json, process_definition_id FROM process_instances WHERE id = ?"); $stmt->execute([$instanceId]); $instance = $stmt->fetch(PDO::FETCH_ASSOC); if (!$instance) { throw new WorkflowNotFoundException("Process instance #$instanceId not found."); } $data = $instance['data_json'] ? json_decode($instance['data_json'], true) : []; // Update the specific task status $data[$taskCode] = $isChecked; $newDataJson = json_encode($data); // Save new data_json and update timestamp $stmt = $this->pdo->prepare("UPDATE process_instances SET data_json = ?, last_activity_at = CURRENT_TIMESTAMP WHERE id = ?"); $stmt->execute([$newDataJson, $instanceId]); // Add an event for the checklist update $message = "Checklist task '$taskCode' marked as " . ($isChecked ? 'complete' : 'incomplete') . "."; $this->addEvent($instanceId, 'checklist_update', $message, null, ['task' => $taskCode, 'checked' => $isChecked], $userId); // Calculate progress $stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?"); $stmt_def->execute([$instance['process_definition_id']]); $definitionJson = $stmt_def->fetchColumn(); $definition = json_decode($definitionJson, true); $totalTasks = count($definition['tasks'] ?? []); $completedTasks = count(array_filter($data)); $this->pdo->commit(); return [ 'success' => true, 'progress' => [ 'completed' => $completedTasks, 'total' => $totalTasks ], 'lastActivityAt' => date('d/m/y') ]; } catch (Exception $e) { $this->pdo->rollBack(); throw $e; } } private function addEvent(int $instanceId, string $eventType, string $message, ?string $nodeId, array $payload, int $userId): void { $stmt = $this->pdo->prepare( "INSERT INTO process_events (process_instance_id, event_type, message, node_id, payload_json, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())" ); $stmt->execute([$instanceId, $eventType, $message, $nodeId, json_encode($payload), $userId]); } public function getOrCreateInstanceByDefId(int $personId, int $processDefinitionId, int $userId, array $context = []): ?array { if (!is_int($processDefinitionId) || $processDefinitionId <= 0) { throw new InvalidArgumentException("processDefinitionId must be a positive integer."); } if (!is_int($personId) || $personId <= 0) { throw new InvalidArgumentException("personId must be a positive integer."); } $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) { $stmt_def = $this->pdo->prepare("SELECT definition_json, code, is_active FROM process_definitions WHERE id = ?"); $stmt_def->execute([$processDefinitionId]); $definition = $stmt_def->fetch(PDO::FETCH_ASSOC); if (!$definition) { throw new WorkflowNotFoundException("Process definition #$processDefinitionId not found."); } if (empty($definition['is_active'])) { throw new WorkflowNotAllowedException("Process is not active and cannot be started."); } $eligibility = $this->checkEligibility($personId, $processDefinitionId, $context); if (!$eligibility['is_eligible']) { throw new WorkflowEligibilityException("Person is not eligible to start this process.", $eligibility['reasons']); } $definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : []; $processCode = ($definition_json && isset($definition_json['type']) && $definition_json['type'] === 'checklist') ? (string) $processDefinitionId : $definition['code']; if ($processCode) { $instanceId = $this->startProcess($processCode, $personId, $userId); if ($instanceId) { $stmt->execute([$personId, $processDefinitionId]); $instance = $stmt->fetch(PDO::FETCH_ASSOC); } } } return $instance ?: null; } public function getInstanceByDefId(int $personId, int $processDefinitionId): ?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); return $instance ?: null; } public function getEvents(int $instanceId): array { $stmt_events = $this->pdo->prepare("SELECT pe.*, p.email as user_email, p.first_name, p.last_name FROM process_events pe JOIN people p ON pe.created_by = p.id WHERE pe.process_instance_id = ? ORDER BY pe.created_at 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'] ?? []; } public function checkEligibility(int $personId, int $processDefinitionId, array $context = []): array { $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) : []; $reasons = []; if (empty($definition) || empty($definition['eligibility_rules'])) { return ['is_eligible' => true, 'reasons' => []]; } foreach ($definition['eligibility_rules'] as $rule) { try { $params = $rule['params'] ?? $rule; switch ($rule['type']) { case 'checkProcessCompletedRule': case 'process_completed': // Backward compatibility $this->checkProcessCompletedRule($personId, $params); break; case 'checkProcessDataRule': $this->checkProcessDataRule($personId, $params); break; case 'deny_manual_start': $this->checkDenyManualStartRule($context); break; case 'person_property_equals': $this->checkPersonPropertyEqualsRule($personId, $params); break; // Add other rule types here } } catch (WorkflowNotAllowedException $e) { $reasons[] = $e->getMessage(); } } return ['is_eligible' => empty($reasons), 'reasons' => $reasons]; } private function checkDenyManualStartRule(array $context): void { if (!isset($context['source']) || $context['source'] !== 'chain') { throw new WorkflowNotAllowedException("This process can only be started automatically by another process."); } } private function checkPersonPropertyEqualsRule(int $personId, array $params): void { $stmt = $this->pdo->prepare("SELECT * FROM people WHERE id = ?"); $stmt->execute([$personId]); $person = $stmt->fetch(PDO::FETCH_ASSOC); if (!$person) { throw new WorkflowNotAllowedException("Person not found."); } $property = $params['property']; $expectedValue = $params['value']; if (!isset($person[$property])) { throw new WorkflowNotAllowedException("Property '{$property}' not found on person."); } if ($person[$property] !== $expectedValue) { throw new WorkflowNotAllowedException("Person's property '{$property}' is not '{$expectedValue}'."); } } private function checkProcessCompletedRule(int $personId, array $params): void { $stmt = $this->pdo->prepare("\n SELECT pi.id\n FROM process_instances pi\n JOIN process_definitions pd ON pi.process_definition_id = pd.id\n WHERE pi.person_id = ? AND pd.code = ? AND pi.current_status = ?\n ORDER BY pi.last_activity_at DESC\n LIMIT 1\n "); $stmt->execute([$personId, $params['process_code'], $params['expected_status']]); $instance = $stmt->fetch(PDO::FETCH_ASSOC); if (!$instance) { throw new WorkflowNotAllowedException("Prerequisite process '{$params['process_code']}' not completed with status '{$params['expected_status']}'."); } } private function checkProcessDataRule(int $personId, array $params): void { $stmt = $this->pdo->prepare(" SELECT pi.data_json FROM process_instances pi JOIN process_definitions pd ON pi.process_definition_id = pd.id WHERE pi.person_id = ? AND pd.code = ? AND pi.current_status = ? ORDER BY pi.last_activity_at DESC LIMIT 1 "); $stmt->execute([$personId, $params['process_code'], $params['expected_status']]); $data_json = $stmt->fetchColumn(); if (!$data_json) { throw new WorkflowNotAllowedException("Not eligible to start this process. Prerequisite process '{$params['process_code']}' not found with status '{$params['expected_status']}'."); } $data = json_decode($data_json, true); if (!is_array($data)) { $data = []; } foreach ($params['expected_data'] as $key => $expected_value) { if (!isset($data[$key]) || $data[$key] !== $expected_value) { throw new WorkflowNotAllowedException("Not eligible. Condition not met: '$key' is not '$expected_value'."); } } } private function checkTransitionCondition(array $transition, array $instanceData): bool { if (!isset($transition['condition'])) { // A transition without a condition is not automatic, but it is valid to pass through. // The calling context (findAutomaticTransition) will decide if this is an error. return true; } $condition = $transition['condition']; $data = isset($instanceData['data_json']) ? json_decode($instanceData['data_json'], true) : []; $field = $condition['field'] ?? null; $expectedValue = $condition['value'] ?? null; if ($field === null || $expectedValue === null) { // Malformed condition return false; } return isset($data[$field]) && $data[$field] === $expectedValue; } private function executeStartProcessAction(int $personId, array $action, int $userId): void { $stmt = $this->pdo->prepare("SELECT id FROM process_definitions WHERE code = ?"); $stmt->execute([$action['process_code']]); $processDefinitionId = $stmt->fetchColumn(); if ($processDefinitionId) { $this->getOrCreateInstanceByDefId($personId, $processDefinitionId, $userId, ['source' => 'chain']); } } private function executeSetDataAction(int $instanceId, array &$instance, array $action, array $payload): void { $dataJson = $instance['data_json']; $data = $dataJson ? json_decode($dataJson, true) : []; if (isset($action['params']['keys']) && is_array($action['params']['keys'])) { foreach ($action['params']['keys'] as $key) { if (array_key_exists($key, $payload)) { $data[$key] = $payload[$key]; } } } $newDataJson = json_encode($data); // Update the database $stmt_update = $this->pdo->prepare("UPDATE process_instances SET data_json = ? WHERE id = ?"); $stmt_update->execute([$newDataJson, $instanceId]); // Also update the in-memory instance for the next step in the chain $instance['data_json'] = $newDataJson; } public function deleteInstance(int $instanceId): void { $this->pdo->beginTransaction(); try { // Delete events $stmt_events = $this->pdo->prepare("DELETE FROM process_events WHERE process_instance_id = ?"); $stmt_events->execute([$instanceId]); // Delete instance $stmt_instance = $this->pdo->prepare("DELETE FROM process_instances WHERE id = ?"); $stmt_instance->execute([$instanceId]); $this->pdo->commit(); } catch (Exception $e) { $this->pdo->rollBack(); throw $e; } } }