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);
+
+
+
+ = t('modal.process_completed', 'Proces zakończony.') ?>
+ Obecny status: = htmlspecialchars($instance['current_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 = """
+
+
+
+
+ = t('modal.process_completed', 'Proces zakończony.') ?>
+ Obecny status: = htmlspecialchars($instance['current_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.");