From b1016e2a541135df74a27407274487c58b88be8d Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 2 Mar 2026 07:38:53 +0000 Subject: [PATCH] Wiele razy ten sam proces do osoby --- WorkflowEngine.php | 78 +++++++++- _get_instance_details.php | 51 +++++++ _init_single_instance.php | 7 +- _save_process_definition.php | 35 +++-- db/migrations/036_process_versioning.php | 57 +++++++ index.php | 7 + patch2.py | 17 +++ patch_engine.py | 71 +++++++++ patch_index.py | 21 +++ patch_init.py | 59 +++++++ patch_modal.py | 73 +++++++++ patch_save.py | 60 ++++++++ process_definitions.php | 2 +- temp_matrix.php | 187 +++++++++++++++++++++++ 14 files changed, 707 insertions(+), 18 deletions(-) create mode 100644 db/migrations/036_process_versioning.php create mode 100644 patch2.py create mode 100644 patch_engine.py create mode 100644 patch_index.py create mode 100644 patch_init.py create mode 100644 patch_modal.py create mode 100644 patch_save.py create mode 100644 temp_matrix.php diff --git a/WorkflowEngine.php b/WorkflowEngine.php index 01467be..3a541f9 100644 --- a/WorkflowEngine.php +++ b/WorkflowEngine.php @@ -57,7 +57,7 @@ class WorkflowEngine { $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 = $this->pdo->prepare("SELECT id, code, name, definition_json, is_active FROM process_definitions WHERE is_active = 1 AND is_latest = 1 ORDER BY sort_order, name"); $stmt_defs->execute(); $process_definitions_raw = $stmt_defs->fetchAll(PDO::FETCH_ASSOC); @@ -66,6 +66,7 @@ class WorkflowEngine { foreach ($process_definitions_raw as $def) { $definitions[$def['id']] = [ 'id' => $def['id'], + 'code' => $def['code'], 'name' => $def['name'], 'is_active' => $def['is_active'] ]; @@ -122,7 +123,16 @@ class WorkflowEngine { $enriched_instance['computed_next_step'] = $instance['suggested_next_step']; } - $instances[$instance['person_id']][$def_id] = $enriched_instance; + +// Use process_code to map to the latest active column + $code = $instance['process_code'] ?? null; + if ($code && isset($codeToIdMap[$code])) { + $latestDefId = $codeToIdMap[$code]; + // Only keep the most recently active instance per code (since ordered by last_activity_at DESC) + if (!isset($instances[$instance['person_id']][$latestDefId])) { + $instances[$instance['person_id']][$latestDefId] = $enriched_instance; + } + } } } @@ -513,6 +523,49 @@ class WorkflowEngine { $stmt->execute([$instanceId, $eventType, $message, $nodeId, json_encode($payload), $userId]); } + public function createNewInstance(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_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) : []; + $start_node = $definition_json['start_node_id'] ?? 'start'; + $initial_data_map = $definition_json['initial_data'] ?? []; + $data_json = !empty($initial_data_map) ? json_encode($initial_data_map) : null; + + $stmt = $this->pdo->prepare( + "INSERT INTO process_instances (person_id, process_definition_id, current_node_id, current_status, data_json, last_activity_at) VALUES (?, ?, ?, 'in_progress', ?, NOW())" + ); + $stmt->execute([$personId, $processDefinitionId, $start_node, $data_json]); + $newInstanceId = $this->pdo->lastInsertId(); + + $this->logEvent($newInstanceId, 'process_started', 'Process started', $start_node, ['context' => $context], $userId); + + $stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE id = ?"); + $stmt->execute([$newInstanceId]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } + 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."); @@ -521,9 +574,7 @@ class WorkflowEngine { 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); + $instance = $this->getInstanceByDefId($personId, $processDefinitionId); if (!$instance) { $stmt_def = $this->pdo->prepare("SELECT definition_json, code, is_active FROM process_definitions WHERE id = ?"); @@ -562,8 +613,21 @@ class WorkflowEngine { } 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]); + $stmt_code = $this->pdo->prepare("SELECT code FROM process_definitions WHERE id = ?"); + $stmt_code->execute([$processDefinitionId]); + $code = $stmt_code->fetchColumn(); + + if (!$code) return null; + + $stmt = $this->pdo->prepare(" + SELECT pi.* + FROM process_instances pi + JOIN process_definitions pd ON pi.process_definition_id = pd.id + WHERE pi.person_id = ? AND pd.code = ? + ORDER BY pi.last_activity_at DESC, pi.id DESC + LIMIT 1 + "); + $stmt->execute([$personId, $code]); $instance = $stmt->fetch(PDO::FETCH_ASSOC); return $instance ?: null; } diff --git a/_get_instance_details.php b/_get_instance_details.php index 185f1b8..cdf1440 100644 --- a/_get_instance_details.php +++ b/_get_instance_details.php @@ -54,6 +54,57 @@ $instance = $engine->getInstanceByDefId($person_id, $process_definition_id); + +
+
+ + Obecny status: . +
+ +
+ + + getEvents($instanceId); diff --git a/_init_single_instance.php b/_init_single_instance.php index 8aa87f0..3305205 100644 --- a/_init_single_instance.php +++ b/_init_single_instance.php @@ -42,7 +42,12 @@ if($deleteExisting === '1') { // 3. Checking if the person is eligible. // 4. Creating the instance if it doesn't exist. // It will throw specific exceptions (WorkflowNotFoundException, WorkflowNotAllowedException, WorkflowEligibilityException) which our ErrorHandler will turn into 404, 409, and 422 responses. -$instance = $engine->getOrCreateInstanceByDefId($personId, $processDefinitionId, $userId); + = filter_input(INPUT_POST, 'force', FILTER_VALIDATE_INT); +if ( === 1) { + = ->createNewInstance(, , ); +} else { + = ->getOrCreateInstanceByDefId(, , ); +} if ($instance) { echo json_encode(['success' => true, 'message' => 'Process initialized successfully.', 'instance_id' => $instance['id']]); diff --git a/_save_process_definition.php b/_save_process_definition.php index 0693dd5..b3ec95b 100644 --- a/_save_process_definition.php +++ b/_save_process_definition.php @@ -108,19 +108,36 @@ try { if (empty($processId)) { // Create new process - $sql = 'INSERT INTO process_definitions (name, code, definition_json, start_node_id, is_active) VALUES (?, ?, ?, ?, 1)'; + $sql = 'INSERT INTO process_definitions (name, code, definition_json, start_node_id, is_active, version, is_latest) VALUES (?, ?, ?, ?, 1, 1, 1)'; $params = [$name, $code, $definition_json, $start_node]; $message = 'Process created successfully.'; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); } else { - // Update existing process - $is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 0; - $sql = 'UPDATE process_definitions SET name = ?, definition_json = ?, start_node_id = ?, is_active = ? WHERE id = ?'; - $params = [$name, $definition_json, $start_node, $is_active, $processId]; - $message = 'Process updated successfully.'; - } + // "Update" existing process by creating a new version + $stmt_old = $pdo->prepare('SELECT code, version, sort_order, is_active FROM process_definitions WHERE id = ?'); + $stmt_old->execute([$processId]); + $old = $stmt_old->fetch(); + + if ($old) { + $is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : $old['is_active']; + $new_version = $old['version'] + 1; + $db_code = $old['code']; - $stmt = $pdo->prepare($sql); - $stmt->execute($params); + // Mark all previous versions as not latest + $stmt_update = $pdo->prepare('UPDATE process_definitions SET is_latest = 0 WHERE code = ?'); + $stmt_update->execute([$db_code]); + + // Insert new version + $sql = 'INSERT INTO process_definitions (name, code, definition_json, start_node_id, is_active, version, supersedes_definition_id, is_latest, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?)'; + $params = [$name, $db_code, $definition_json, $start_node, $is_active, $new_version, $processId, $old['sort_order']]; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $message = 'Process updated successfully (new version created).'; + } else { + throw new WorkflowRuleFailedException('Process not found.'); + } + } if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) { header('Content-Type: application/json'); echo json_encode(['message' => $message]); diff --git a/db/migrations/036_process_versioning.php b/db/migrations/036_process_versioning.php new file mode 100644 index 0000000..5dda8e8 --- /dev/null +++ b/db/migrations/036_process_versioning.php @@ -0,0 +1,57 @@ +query("SHOW INDEX FROM process_definitions WHERE Key_name = 'code'"); + if ($stmt->fetch()) { + $pdo->exec("ALTER TABLE process_definitions DROP INDEX `code`"); + echo "Dropped unique index on process_definitions.code. +"; + } + + $stmt = $pdo->query("SHOW COLUMNS FROM process_definitions LIKE 'supersedes_definition_id'"); + if (!$stmt->fetch()) { + $pdo->exec("ALTER TABLE process_definitions ADD COLUMN supersedes_definition_id INT(11) UNSIGNED NULL AFTER version"); + echo "Added supersedes_definition_id to process_definitions. +"; + } + + $stmt = $pdo->query("SHOW COLUMNS FROM process_definitions LIKE 'is_latest'"); + if (!$stmt->fetch()) { + $pdo->exec("ALTER TABLE process_definitions ADD COLUMN is_latest TINYINT(1) NOT NULL DEFAULT 1 AFTER supersedes_definition_id"); + echo "Added is_latest to process_definitions. +"; + } + + $stmt = $pdo->query("SHOW INDEX FROM process_definitions WHERE Key_name = 'idx_code_latest_active'"); + if (!$stmt->fetch()) { + $pdo->exec("ALTER TABLE process_definitions ADD INDEX idx_code_latest_active (code, is_latest, is_active)"); + echo "Added index idx_code_latest_active on process_definitions. +"; + } + + // Ensure existing rows are marked as latest properly (all existing should be latest) + // Nothing needed, default is 1. + + // 2. Process Instances + $stmt = $pdo->query("SHOW INDEX FROM process_instances WHERE Key_name = 'person_process'"); + if ($stmt->fetch()) { + $pdo->exec("ALTER TABLE process_instances DROP INDEX `person_process`"); + echo "Dropped unique index on process_instances.person_process. +"; + } + + $stmt = $pdo->query("SHOW INDEX FROM process_instances WHERE Key_name = 'idx_person_definition'"); + if (!$stmt->fetch()) { + $pdo->exec("ALTER TABLE process_instances ADD INDEX idx_person_definition (person_id, process_definition_id)"); + echo "Added index idx_person_definition on process_instances. +"; + } + + echo "Migration 036 completed. +"; +} + diff --git a/index.php b/index.php index 9f3392d..4081cff 100644 --- a/index.php +++ b/index.php @@ -1392,6 +1392,13 @@ document.addEventListener('DOMContentLoaded', function () { } }); } + + instanceModalElement.addEventListener('hidden.bs.modal', function () { + if (window.matrixNeedsRefresh) { + window.location.reload(); + } + }); }); + diff --git a/patch2.py b/patch2.py new file mode 100644 index 0000000..a329ee5 --- /dev/null +++ b/patch2.py @@ -0,0 +1,17 @@ +import re + +with open('WorkflowEngine.php', 'r') as f: + content = f.read() + +target = """ $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);""" + +replacement = """ $instance = $this->getInstanceByDefId($personId, $processDefinitionId);""" + +content = content.replace(target, replacement) + +with open('WorkflowEngine.php', 'w') as f: + f.write(content) + +print("Patched.") diff --git a/patch_engine.py b/patch_engine.py new file mode 100644 index 0000000..5fe00bd --- /dev/null +++ b/patch_engine.py @@ -0,0 +1,71 @@ +import re + +with open('WorkflowEngine.php', 'r') as f: + content = f.read() + +content = content.replace( + 'SELECT id, name, definition_json, is_active FROM process_definitions WHERE is_active = 1 ORDER BY sort_order, name', + 'SELECT id, code, name, definition_json, is_active FROM process_definitions WHERE is_active = 1 AND is_latest = 1 ORDER BY sort_order, name' +) + +content = content.replace( + "'name' => $def['name'],", + "'code' => $def['code'],\n 'name' => $def['name']," +) + +content = content.replace( + '$stmt_instances = $this->pdo->prepare("SELECT * FROM process__instances WHERE person_id IN ($placeholders)");\n $stmt_instances->execute($person_ids);\n $instances_data = $stmt_instances->fetchAll(PDO::FETCH_ASSOC);', + """ +$stmt_instances = $this->pdo->prepare( + SELECT pi.*, pd.code as process_code, pd.id as definition_actual_id + FROM process_instances pi + JOIN process_definitions pd ON pi.process_definition_id = pd.id + WHERE pi.person_id IN ($placeholders) + ORDER BY pi.last_activity_at DESC, pi.id DESC + "); + $stmt_instances->execute($person_ids); + $instances_data = $stmt_instances->fetchAll(PDO::FETCH_ASSOC); + + // Map code to latest definition id + $codeToIdMap = []; + foreach ($definitions as $defId => $def) { + $codeToIdMap[$def['code']] = $defId; + }""") + +content = content.replace( + "$instances[$instance['person_id']][$def_id] = $enriched_instance;", + """ +// Use process_code to map to the latest active column + $code = $instance['process_code'] ?? null; + if ($code && isset($codeToIdMap[$code])) { + $latestDefId = $codeToIdMap[$code]; + // Only keep the most recently active instance per code (since ordered by last_activity_at DESC) + if (!isset($instances[$instance['person_id']][$latestDefId])) { + $instances[$instance['person_id']][$latestDefId] = $enriched_instance; + } + }""") + +content = re.sub( + r"public function getInstanceByDefId\(int \$personId, int \$processDefinitionId\): \?array \{.*?return \$instance \?: null;\s*\}", + """public function getInstanceByDefId(int $personId, int $processDefinitionId): ?array { + $stmt_code = $this->pdo->prepare("SELECT code FROM process_definitions WHERE id = ?"); + $stmt_code->execute([$processDefinitionId]); + $code = $stmt_code->fetchColumn(); + + if (!$code) return null; + + $stmt = $this->pdo->prepare("\n SELECT pi.* \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 = ?\n ORDER BY pi.last_activity_at DESC, pi.id DESC\n LIMIT 1\n "); + $stmt->execute([$personId, $code]); + $instance = $stmt->fetch(PDO::FETCH_ASSOC); + return $instance ?: null; + }""", + content, flags=re.DOTALL +) + +# Wait, checkEligibility needs to use the NEW version definition ID? +# Actually checkEligibility uses $processDefinitionId which IS the new version's ID! + +with open('WorkflowEngine.php', 'w') as f: + f.write(content) + +print("Patched.") \ No newline at end of file diff --git a/patch_index.py b/patch_index.py new file mode 100644 index 0000000..9505e09 --- /dev/null +++ b/patch_index.py @@ -0,0 +1,21 @@ +import re + +with open('index.php', 'r') as f: + content = f.read() + +patch_code = """ + instanceModalElement.addEventListener('hidden.bs.modal', function () { + if (window.matrixNeedsRefresh) { + window.location.reload(); + } + }); +}); + +""" + +content = content.replace("});\n", patch_code) + +with open('index.php', 'w') as f: + f.write(content) + +print("Patched index.php") diff --git a/patch_init.py b/patch_init.py new file mode 100644 index 0000000..f66e74a --- /dev/null +++ b/patch_init.py @@ -0,0 +1,59 @@ +import re + +with open('WorkflowEngine.php', 'r') as f: + content = f.read() + +new_method = """ + public function createNewInstance(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_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) : []; + $start_node = $definition_json['start_node_id'] ?? 'start'; + $initial_data_map = $definition_json['initial_data'] ?? []; + $data_json = !empty($initial_data_map) ? json_encode($initial_data_map) : null; + + $stmt = $this->pdo->prepare( + "INSERT INTO process_instances (person_id, process_definition_id, current_node_id, current_status, data_json, last_activity_at) VALUES (?, ?, ?, 'in_progress', ?, NOW())" + ); + $stmt->execute([$personId, $processDefinitionId, $start_node, $data_json]); + $newInstanceId = $this->pdo->lastInsertId(); + + $this->logEvent($newInstanceId, 'process_started', 'Process started', $start_node, ['context' => $context], $userId); + + $stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE id = ?"); + $stmt->execute([$newInstanceId]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } +" + +content = content.replace( + 'public function getOrCreateInstanceByDefId(int $personId, int $processDefinitionId, int $userId, array $context = []): ?array {', + new_method + '\n public function getOrCreateInstanceByDefId(int $personId, int $processDefinitionId, int $userId, array $context = []): ?array {' +) + +with open('WorkflowEngine.php', 'w') as f: + f.write(content) + +print("Patched.") diff --git a/patch_modal.py b/patch_modal.py new file mode 100644 index 0000000..853538d --- /dev/null +++ b/patch_modal.py @@ -0,0 +1,73 @@ +import re + +with open('_get_instance_details.php', 'r') as f: + content = f.read() + +patch_code = """ + + +
+
+ + Obecny status: . +
+ +
+ + + + prepare($sql); + $stmt->execute($params);""" + +new_update_logic = """ if (empty($processId)) { + // Create new process + $sql = 'INSERT INTO process_definitions (name, code, definition_json, start_node_id, is_active, version, is_latest) VALUES (?, ?, ?, ?, 1, 1, 1)'; + $params = [$name, $code, $definition_json, $start_node]; + $message = 'Process created successfully.'; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + } else { + // "Update" existing process by creating a new version + $stmt_old = $pdo->prepare('SELECT code, version, sort_order, is_active FROM process_definitions WHERE id = ?'); + $stmt_old->execute([$processId]); + $old = $stmt_old->fetch(); + + if ($old) { + $is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : $old['is_active']; + $new_version = $old['version'] + 1; + $db_code = $old['code']; + + // Mark all previous versions as not latest + $stmt_update = $pdo->prepare('UPDATE process_definitions SET is_latest = 0 WHERE code = ?'); + $stmt_update->execute([$db_code]); + + // Insert new version + $sql = 'INSERT INTO process_definitions (name, code, definition_json, start_node_id, is_active, version, supersedes_definition_id, is_latest, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?)'; + $params = [$name, $db_code, $definition_json, $start_node, $is_active, $new_version, $processId, $old['sort_order']]; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $message = 'Process updated successfully (new version created).'; + } else { + throw new WorkflowRuleFailedException('Process not found.'); + } + }""" + +content = content.replace(old_update_logic, new_update_logic) + +with open('_save_process_definition.php', 'w') as f: + f.write(content) + +print("Patched.") diff --git a/process_definitions.php b/process_definitions.php index 04b054d..69f7535 100644 --- a/process_definitions.php +++ b/process_definitions.php @@ -4,7 +4,7 @@ require_once 'WorkflowEngine.php'; $workflowEngine = new WorkflowEngine(); // TODO: Create a method in WorkflowEngine to get all process definitions $pdo = db(); -$stmt = $pdo->query("SELECT * FROM process_definitions WHERE is_active = 1 ORDER BY sort_order, name"); +$stmt = $pdo->query("SELECT * FROM process_definitions WHERE is_active = 1 AND is_latest = 1 ORDER BY sort_order, name"); $processes = $stmt->fetchAll(PDO::FETCH_ASSOC); ?> diff --git a/temp_matrix.php b/temp_matrix.php new file mode 100644 index 0000000..2568970 --- /dev/null +++ b/temp_matrix.php @@ -0,0 +1,187 @@ + public function getDashboardMatrix(?string $searchTerm = null, ?int $groupId = null, ?int $activeProcessDefinitionId = null, ?int $meetingFilterGroupId = null, ?string $meetingFilterDatetime = 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 ($meetingFilterGroupId && $meetingFilterDatetime) { + $meetingId = $this->getOrCreateMeeting($meetingFilterGroupId, $meetingFilterDatetime); + $sql_people .= " INNER JOIN meeting_attendance ma ON p.id = ma.person_id"; + $where_clauses[] = "ma.meeting_id = :meeting_id"; + $where_clauses[] = "ma.attendance_status IN ('present', 'absent', 'substitute')"; + $params[':meeting_id'] = $meetingId; + } + + 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, code, name, definition_json, is_active FROM process_definitions WHERE is_active = 1 AND is_latest = 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'], + 'code' => $def['code'], + '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 { + $inTransaction = $this->pdo->inTransaction(); + if (!$inTransaction) { $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.");