Wiele razy ten sam proces do osoby

This commit is contained in:
Flatlogic Bot 2026-03-02 07:38:53 +00:00
parent 7717562504
commit b1016e2a54
14 changed files with 707 additions and 18 deletions

View File

@ -57,7 +57,7 @@ class WorkflowEngine {
$people = $stmt_people->fetchAll(PDO::FETCH_ASSOC); $people = $stmt_people->fetchAll(PDO::FETCH_ASSOC);
// 4. Fetch all process definitions with their JSON // 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(); $stmt_defs->execute();
$process_definitions_raw = $stmt_defs->fetchAll(PDO::FETCH_ASSOC); $process_definitions_raw = $stmt_defs->fetchAll(PDO::FETCH_ASSOC);
@ -66,6 +66,7 @@ class WorkflowEngine {
foreach ($process_definitions_raw as $def) { foreach ($process_definitions_raw as $def) {
$definitions[$def['id']] = [ $definitions[$def['id']] = [
'id' => $def['id'], 'id' => $def['id'],
'code' => $def['code'],
'name' => $def['name'], 'name' => $def['name'],
'is_active' => $def['is_active'] 'is_active' => $def['is_active']
]; ];
@ -122,7 +123,16 @@ class WorkflowEngine {
$enriched_instance['computed_next_step'] = $instance['suggested_next_step']; $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]); $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 { public function getOrCreateInstanceByDefId(int $personId, int $processDefinitionId, int $userId, array $context = []): ?array {
if (!is_int($processDefinitionId) || $processDefinitionId <= 0) { if (!is_int($processDefinitionId) || $processDefinitionId <= 0) {
throw new InvalidArgumentException("processDefinitionId must be a positive integer."); throw new InvalidArgumentException("processDefinitionId must be a positive integer.");
@ -521,9 +574,7 @@ class WorkflowEngine {
throw new InvalidArgumentException("personId must be a positive integer."); throw new InvalidArgumentException("personId must be a positive integer.");
} }
$stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE `person_id` = ? AND `process_definition_id` = ?"); $instance = $this->getInstanceByDefId($personId, $processDefinitionId);
$stmt->execute([$personId, $processDefinitionId]);
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$instance) { if (!$instance) {
$stmt_def = $this->pdo->prepare("SELECT definition_json, code, is_active FROM process_definitions WHERE id = ?"); $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 { public function getInstanceByDefId(int $personId, int $processDefinitionId): ?array {
$stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE `person_id` = ? AND `process_definition_id` = ?"); $stmt_code = $this->pdo->prepare("SELECT code FROM process_definitions WHERE id = ?");
$stmt->execute([$personId, $processDefinitionId]); $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); $instance = $stmt->fetch(PDO::FETCH_ASSOC);
return $instance ?: null; return $instance ?: null;
} }

View File

@ -54,6 +54,57 @@ $instance = $engine->getInstanceByDefId($person_id, $process_definition_id);
<?php if ($instance): // INSTANCE EXISTS ?> <?php if ($instance): // INSTANCE EXISTS ?>
<?php <?php
$instanceId = $instance['id']; $instanceId = $instance['id'];
$isCompleted = in_array($instance['current_status'], ['completed', 'positive', 'negative', 'error']);
?>
<?php if ($isCompleted): ?>
<div class="alert alert-info d-flex justify-content-between align-items-center">
<div>
<strong><?= t('modal.process_completed', 'Proces zakończony.') ?></strong>
Obecny status: <?= htmlspecialchars($instance['current_status']) ?>.
</div>
<button id="startNewProcessBtn" class="btn btn-sm btn-primary" data-person-id="<?= $person_id ?>" data-process-id="<?= $process_definition_id ?>">
<?= t('modal.start_new_instance', 'Rozpocznij od nowa') ?>
</button>
</div>
<script>
document.getElementById('startNewProcessBtn')?.addEventListener('click', function() {
const personId = this.getAttribute('data-person-id');
const processId = this.getAttribute('data-process-id');
if (confirm('Czy na pewno chcesz rozpocząć nową instancję tego procesu? Dotychczasowa historia pozostanie, ale proces rozpocznie się od nowa.')) {
fetch('_init_single_instance.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'person_id=' + personId + '&process_id=' + processId + '&force=1'
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Reload the modal content
fetch('_get_instance_details.php?person_id=' + personId + '&process_id=' + processId)
.then(response => response.text())
.then(html => {
document.querySelector('#instanceModal .modal-body').innerHTML = html;
// Trigger custom event to re-attach handlers and update title
const event = new Event('instanceModalLoaded');
document.querySelector('#instanceModal .modal-body').dispatchEvent(event);
// Set a flag to refresh the matrix when the modal closes
window.matrixNeedsRefresh = true;
});
} else {
alert('Błąd: ' + (data.error || 'Nieznany błąd'));
}
})
.catch(error => {
console.error('Error:', error);
alert('Wystąpił błąd podczas uruchamiania nowej instancji.');
});
}
});
</script>
<?php endif; ?>
<?php
$definition = $process['definition_json'] ? json_decode($process['definition_json'], true) : null; $definition = $process['definition_json'] ? json_decode($process['definition_json'], true) : null;
$isChecklist = ($definition && isset($definition['type']) && $definition['type'] === 'checklist'); $isChecklist = ($definition && isset($definition['type']) && $definition['type'] === 'checklist');
$events = $engine->getEvents($instanceId); $events = $engine->getEvents($instanceId);

View File

@ -42,7 +42,12 @@ if($deleteExisting === '1') {
// 3. Checking if the person is eligible. // 3. Checking if the person is eligible.
// 4. Creating the instance if it doesn't exist. // 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. // 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) { if ($instance) {
echo json_encode(['success' => true, 'message' => 'Process initialized successfully.', 'instance_id' => $instance['id']]); echo json_encode(['success' => true, 'message' => 'Process initialized successfully.', 'instance_id' => $instance['id']]);

View File

@ -108,19 +108,36 @@ try {
if (empty($processId)) { if (empty($processId)) {
// Create new process // 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]; $params = [$name, $code, $definition_json, $start_node];
$message = 'Process created successfully.'; $message = 'Process created successfully.';
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
} else { } else {
// Update existing process // "Update" existing process by creating a new version
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 0; $stmt_old = $pdo->prepare('SELECT code, version, sort_order, is_active FROM process_definitions WHERE id = ?');
$sql = 'UPDATE process_definitions SET name = ?, definition_json = ?, start_node_id = ?, is_active = ? WHERE id = ?'; $stmt_old->execute([$processId]);
$params = [$name, $definition_json, $start_node, $is_active, $processId]; $old = $stmt_old->fetch();
$message = 'Process updated successfully.';
}
$stmt = $pdo->prepare($sql); if ($old) {
$stmt->execute($params); $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.');
}
}
if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) { if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) {
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode(['message' => $message]); echo json_encode(['message' => $message]);

View File

@ -0,0 +1,57 @@
<?php
function migrate_036($pdo) {
echo "Starting migration 036 (Process Versioning)...
";
// 1. Process Definitions
$stmt = $pdo->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.
";
}

View File

@ -1392,6 +1392,13 @@ document.addEventListener('DOMContentLoaded', function () {
} }
}); });
} }
instanceModalElement.addEventListener('hidden.bs.modal', function () {
if (window.matrixNeedsRefresh) {
window.location.reload();
}
});
}); });
</script> </script>
<?php include '_footer.php'; ?> <?php include '_footer.php'; ?>

17
patch2.py Normal file
View File

@ -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.")

71
patch_engine.py Normal file
View File

@ -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.")

21
patch_index.py Normal file
View File

@ -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();
}
});
});
</script>
"""
content = content.replace("});\n</script>", patch_code)
with open('index.php', 'w') as f:
f.write(content)
print("Patched index.php")

59
patch_init.py Normal file
View File

@ -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.")

73
patch_modal.py Normal file
View File

@ -0,0 +1,73 @@
import re
with open('_get_instance_details.php', 'r') as f:
content = f.read()
patch_code = """<?php if ($instance): // INSTANCE EXISTS ?>
<?php
$instanceId = $instance['id'];
$isCompleted = in_array($instance['current_status'], ['completed', 'positive', 'negative', 'error']);
?>
<?php if ($isCompleted): ?>
<div class="alert alert-info d-flex justify-content-between align-items-center">
<div>
<strong><?= t('modal.process_completed', 'Proces zakończony.') ?></strong>
Obecny status: <?= htmlspecialchars($instance['current_status']) ?>.
</div>
<button id="startNewProcessBtn" class="btn btn-sm btn-primary" data-person-id="<?= $person_id ?>" data-process-id="<?= $process_definition_id ?>">
<?= t('modal.start_new_instance', 'Rozpocznij od nowa') ?>
</button>
</div>
<script>
document.getElementById('startNewProcessBtn')?.addEventListener('click', function() {
const personId = this.getAttribute('data-person-id');
const processId = this.getAttribute('data-process-id');
if (confirm('Czy na pewno chcesz rozpocząć nową instancję tego procesu? Dotychczasowa historia pozostanie, ale proces rozpocznie się od nowa.')) {
fetch('_init_single_instance.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'person_id=' + personId + '&process_id=' + processId + '&force=1'
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Reload the modal content
fetch('_get_instance_details.php?person_id=' + personId + '&process_id=' + processId)
.then(response => response.text())
.then(html => {
document.getElementById('instanceModalBody').innerHTML = html;
// Trigger custom event to re-attach handlers and update title
const event = new Event('instanceModalLoaded');
document.getElementById('instanceModalBody').dispatchEvent(event);
// Set a flag to refresh the matrix when the modal closes
window.matrixNeedsRefresh = true;
});
} else {
alert('Błąd: ' + (data.error || 'Nieznany błąd'));
}
})
.catch(error => {
console.error('Error:', error);
alert('Wystąpił błąd podczas uruchamiania nowej instancji.');
});
}
});
</script>
<?php endif; ?>
<?php
$definition = $process['definition_json'] ? json_decode($process['definition_json'], true) : null;"""
content = content.replace(
"""<?php if ($instance): // INSTANCE EXISTS ?>
<?php
$instanceId = $instance['id'];
$definition = $process['definition_json'] ? json_decode($process['definition_json'], true) : null;""",
patch_code
)
with open('_get_instance_details.php', 'w') as f:
f.write(content)
print("Patched.")

60
patch_save.py Normal file
View File

@ -0,0 +1,60 @@
import re
with open('_save_process_definition.php', 'r') as f:
content = f.read()
old_update_logic = """ if (empty($processId)) {
// Create new process
$sql = 'INSERT INTO process_definitions (name, code, definition_json, start_node_id, is_active) VALUES (?, ?, ?, ?, 1)';
$params = [$name, $code, $definition_json, $start_node];
$message = 'Process created successfully.';
} 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.';
}
$stmt = $pdo->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.")

View File

@ -4,7 +4,7 @@ require_once 'WorkflowEngine.php';
$workflowEngine = new WorkflowEngine(); $workflowEngine = new WorkflowEngine();
// TODO: Create a method in WorkflowEngine to get all process definitions // TODO: Create a method in WorkflowEngine to get all process definitions
$pdo = db(); $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); $processes = $stmt->fetchAll(PDO::FETCH_ASSOC);
?> ?>

187
temp_matrix.php Normal file
View File

@ -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.");