Głęboka refactoryzacja kodu

This commit is contained in:
Flatlogic Bot 2026-01-10 20:46:53 +00:00
parent a703aeb1e2
commit 3b1a26adc9
4 changed files with 372 additions and 215 deletions

View File

@ -1,6 +1,7 @@
<?php
// Cache-buster: 1720638682
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/lib/WorkflowExceptions.php';
class WorkflowEngine {
@ -31,7 +32,8 @@ class WorkflowEngine {
foreach ($process_definitions_raw as $def) {
$definitions[$def['id']] = [
'id' => $def['id'],
'name' => $def['name']
'name' => $def['name'],
'is_active' => $def['is_active']
];
$definition_map[$def['id']] = !empty($def['definition_json']) ? json_decode($def['definition_json'], true) : null;
}
@ -87,20 +89,18 @@ class WorkflowEngine {
$instances[$instance['person_id']][$def_id] = $enriched_instance;
}
// Remove pre-emptive eligibility check. This is now handled on-demand by _get_instance_details.php
/*
foreach ($people as $person) {
foreach ($definitions as $def) {
if (!isset($instances[$person['id']][$def['id']])) {
$is_eligible = true;
try {
$process_definition = $process_definitions_raw[array_search($def['id'], array_column($process_definitions_raw, 'id'))];
$this->checkEligibility($person['id'], $process_definition);
} catch (WorkflowNotAllowedException $e) {
$is_eligible = false;
}
$instances[$person['id']][$def['id']] = ['is_eligible' => $is_eligible];
$process_definition_raw = $process_definitions_raw[array_search($def['id'], array_column($process_definitions_raw, 'id'))];
$eligibility = $this->checkEligibility($person['id'], $process_definition_raw);
$instances[$person['id']][$def['id']] = ['is_eligible' => $eligibility['is_eligible']];
}
}
}
*/
// Fetch ancillary data
$stmt_functions = $this->pdo->query("SELECT * FROM functions ORDER BY display_order");
@ -253,6 +253,8 @@ class WorkflowEngine {
foreach ($transition['actions'] as $action) {
if ($action['type'] === 'start_process') {
$this->executeStartProcessAction($instance['person_id'], $action, $userId);
} elseif ($action['type'] === 'set_data') {
$this->executeSetDataAction($instanceId, $action);
}
}
}
@ -436,6 +438,13 @@ class WorkflowEngine {
return $instance ?: null;
}
public function getInstanceByDefId(int $personId, int $processDefinitionId): ?array {
$stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE `person_id` = ? AND `process_definition_id` = ?");
$stmt->execute([$personId, $processDefinitionId]);
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
return $instance ?: null;
}
public function getEvents(int $instanceId): array {
$stmt_events = $this->pdo->prepare("SELECT pe.*, p.email as user_email, p.first_name, p.last_name FROM process_events pe JOIN people p ON pe.created_by = p.id WHERE pe.process_instance_id = ? ORDER BY pe.created_at DESC");
$stmt_events->execute([$instanceId]);
@ -476,44 +485,101 @@ class WorkflowEngine {
return $definition['nodes'] ?? [];
}
private function checkEligibility(int $personId, array $definition): void {
$definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : [];
if (empty($definition_json) || empty($definition_json['eligibility_rules'])) {
return; // No rules to check
public function checkEligibility(int $personId, int $processDefinitionId): array {
$stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?");
$stmt_def->execute([$processDefinitionId]);
$definition_json = $stmt_def->fetchColumn();
$definition = !empty($definition_json) ? json_decode($definition_json, true) : [];
$reasons = [];
if (empty($definition) || empty($definition['eligibility_rules'])) {
return ['is_eligible' => true, 'reasons' => []];
}
foreach ($definition_json['eligibility_rules'] as $rule) {
foreach ($definition['eligibility_rules'] as $rule) {
try {
$params = $rule['params'] ?? $rule;
switch ($rule['type']) {
case 'process_completed':
$this->checkProcessCompletedRule($personId, $rule);
case 'checkProcessCompletedRule':
case 'process_completed': // Backward compatibility
$this->checkProcessCompletedRule($personId, $params);
break;
case 'checkProcessDataRule':
$this->checkProcessDataRule($personId, $params);
break;
// Add other rule types here
}
} catch (WorkflowNotAllowedException $e) {
$reasons[] = $e->getMessage();
}
}
private function checkProcessCompletedRule(int $personId, array $rule): void {
$stmt = $this->pdo->prepare("
SELECT pi.id
FROM process_instances pi
JOIN process_definitions pd ON pi.process_definition_id = pd.id
WHERE pi.person_id = ? AND pd.name = ? AND pi.current_status = ?
");
$stmt->execute([$personId, $rule['process_name'], $rule['expected_status']]);
return ['is_eligible' => empty($reasons), 'reasons' => $reasons];
}
private function checkProcessCompletedRule(int $personId, array $params): void {
$stmt = $this->pdo->prepare("\n SELECT pi.id\n FROM process_instances pi\n JOIN process_definitions pd ON pi.process_definition_id = pd.id\n WHERE pi.person_id = ? AND pd.code = ? AND pi.current_status = ?\n ORDER BY pi.last_activity_at DESC\n LIMIT 1\n ");
$stmt->execute([$personId, $params['process_code'], $params['expected_status']]);
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$instance) {
throw new WorkflowNotAllowedException("Not eligible to start this process. Prerequisite process '{$rule['process_name']}' not completed with status '{$rule['expected_status']}'.");
throw new WorkflowNotAllowedException("Prerequisite process '{$params['process_code']}' not completed with status '{$params['expected_status']}'.");
}
}
private function checkProcessDataRule(int $personId, array $params): void {
$stmt = $this->pdo->prepare("
SELECT pi.data_json
FROM process_instances pi
JOIN process_definitions pd ON pi.process_definition_id = pd.id
WHERE pi.person_id = ? AND pd.code = ? AND pi.current_status = ?
ORDER BY pi.last_activity_at DESC
LIMIT 1
");
$stmt->execute([$personId, $params['process_code'], $params['expected_status']]);
$data_json = $stmt->fetchColumn();
if (!$data_json) {
throw new WorkflowNotAllowedException("Not eligible to start this process. Prerequisite process '{$params['process_code']}' not found with status '{$params['expected_status']}'.");
}
$data = json_decode($data_json, true);
if (!is_array($data)) {
$data = [];
}
foreach ($params['expected_data'] as $key => $expected_value) {
if (!isset($data[$key]) || $data[$key] !== $expected_value) {
throw new WorkflowNotAllowedException("Not eligible. Condition not met: '$key' is not '$expected_value'.");
}
}
}
private function executeStartProcessAction(int $personId, array $action, int $userId): void {
$stmt = $this->pdo->prepare("SELECT id FROM process_definitions WHERE name = ?");
$stmt->execute([$action['process_name']]);
$stmt = $this->pdo->prepare("SELECT id FROM process_definitions WHERE code = ?");
$stmt->execute([$action['process_code']]);
$processDefinitionId = $stmt->fetchColumn();
if ($processDefinitionId) {
$this->getOrCreateInstanceByDefId($personId, $processDefinitionId, $userId);
}
}
private function executeSetDataAction(int $instanceId, array $action): void {
$stmt = $this->pdo->prepare("SELECT data_json FROM process_instances WHERE id = ?");
$stmt->execute([$instanceId]);
$dataJson = $stmt->fetchColumn();
$data = $dataJson ? json_decode($dataJson, true) : [];
$key = $action['params']['key'];
$value = $action['params']['value'];
$data[$key] = $value;
$newDataJson = json_encode($data);
$stmt_update = $this->pdo->prepare("UPDATE process_instances SET data_json = ? WHERE id = ?");
$stmt_update->execute([$newDataJson, $instanceId]);
}
}

View File

@ -7,28 +7,25 @@ session_start();
// Security check
if (!isset($_SESSION['user_id'])) {
throw new WorkflowNotAllowedException('Brak autoryzacji');
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
$person_id = $_GET['person_id'] ?? null;
$process_definition_id = $_GET['process_id'] ?? null;
if (!$person_id || !$process_definition_id) {
throw new WorkflowRuleFailedException('Brakujące parametry');
http_response_code(400);
echo json_encode(['error' => 'Missing person_id or process_id']);
exit;
}
$userId = $_SESSION['user_id'];
$engine = new WorkflowEngine();
$pdo = db();
// 1. Get or create instance
$instance = $engine->getOrCreateInstanceByDefId($person_id, $process_definition_id, $userId);
if (!$instance) {
throw new WorkflowNotFoundException("Nie można pobrać lub utworzyć instancji procesu.");
}
$instanceId = $instance['id'];
// 2. Fetch all related data
// Fetch Person and Process Definition details first
$stmt_person = $pdo->prepare("SELECT first_name, last_name FROM people WHERE id = ?");
$stmt_person->execute([$person_id]);
$person = $stmt_person->fetch();
@ -36,24 +33,37 @@ $person = $stmt_person->fetch();
$stmt_process = $pdo->prepare("SELECT * FROM process_definitions WHERE id = ?");
$stmt_process->execute([$process_definition_id]);
$process = $stmt_process->fetch();
$definition = $process && $process['definition_json'] ? json_decode($process['definition_json'], true) : null;
$isChecklist = ($definition && isset($definition['type']) && $definition['type'] === 'checklist');
$events = $engine->getEvents($instanceId);
if (!$person || !$process) {
http_response_code(404);
echo "<p class='text-danger'>Could not find person or process.</p>";
exit;
}
// Try to find an existing instance
$instance = $engine->getInstanceByDefId($person_id, $process_definition_id);
?>
<!-- Title for the modal, to be grabbed by JS -->
<div id="instance-modal-title" class="d-none">
<?= htmlspecialchars($person['first_name'] . ' ' . $person['last_name']) ?> - <?= htmlspecialchars($process['name']) ?>
<?= htmlspecialchars($person['first_name']." ".$person['last_name']) ?> - <?= htmlspecialchars($process['name']) ?>
</div>
<?php if ($instance): // INSTANCE EXISTS ?>
<?php
$instanceId = $instance['id'];
$definition = $process['definition_json'] ? json_decode($process['definition_json'], true) : null;
$isChecklist = ($definition && isset($definition['type']) && $definition['type'] === 'checklist');
$events = $engine->getEvents($instanceId);
?>
<?php if ($isChecklist): ?>
<?php
$tasks = $definition['tasks'] ?? [];
$instanceData = $instance && $instance['data_json'] ? json_decode($instance['data_json'], true) : [];
$instanceData = $instance['data_json'] ? json_decode($instance['data_json'], true) : [];
?>
<div class="checklist-modal-container">
<div class="checklist-modal-container" data-instance-id="<?= $instanceId ?>">
<h5>Zadania do wykonania</h5>
<div class="checklist-container">
<?php foreach ($tasks as $task):
@ -69,7 +79,6 @@ $events = $engine->getEvents($instanceId);
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<?php
$currentNodeId = $instance['current_node_id'];
@ -79,7 +88,7 @@ $events = $engine->getEvents($instanceId);
$available_target_node_ids = array_map(function($t) { return $t['to']; }, $availableTransitions);
$available_transitions_map = [];
foreach ($availableTransitions as $t) {
$available_transitions_map[$t['to']] = $t;
$available_transitions_map[$t['id']] = $t;
}
$visited_nodes = [];
@ -92,15 +101,12 @@ $events = $engine->getEvents($instanceId);
<div class="process-steps-container">
<h5>Kroki procesu</h5>
<ul class="list-group">
<?php foreach ($all_nodes as $nodeId => $node): ?>
<?php
<?php foreach ($all_nodes as $nodeId => $node):
$is_current = ($currentNodeId === $nodeId);
$is_completed = isset($visited_nodes[$nodeId]) && !$is_current;
$is_available = in_array($nodeId, $available_target_node_ids);
$status_icon = '';
$li_class = '';
$button = '';
if ($is_current) {
$li_class = 'list-group-item-primary';
@ -110,18 +116,7 @@ $events = $engine->getEvents($instanceId);
$status_icon = '<i class="bi bi-check-circle-fill text-success me-2"></i>';
} else {
$li_class = 'text-muted';
$status_icon = '<i class="bi bi-lock-fill me-2"></i>';
}
if ($is_available) {
$transition = $available_transitions_map[$nodeId];
$button = <<<HTML
<button class="btn btn-sm btn-primary apply-transition-btn"
data-instance-id="{$instanceId}"
data-transition-id="{$transition['id']}">
{$transition['name']}
</button>
HTML;
$status_icon = '<i class="bi bi-circle me-2"></i>';
}
?>
<li class="list-group-item d-flex justify-content-between align-items-center <?= $li_class ?>">
@ -129,10 +124,23 @@ HTML;
<?= $status_icon ?>
<strong><?= htmlspecialchars($node['name']) ?></strong>
</div>
<?= $button ?>
</li>
<?php endforeach; ?>
</ul>
<div class="mt-3">
<h5>Available Actions</h5>
<?php if (empty($availableTransitions)): ?>
<p>No actions available.</p>
<?php else: ?>
<?php foreach ($availableTransitions as $transition): ?>
<button class="btn btn-sm btn-primary apply-transition-btn"
data-instance-id="<?= $instanceId ?>"
data-transition-id="<?= $transition['id'] ?>">
<?= htmlspecialchars($transition['name']) ?>
</button>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
@ -171,5 +179,26 @@ HTML;
<?php endif; ?>
</div>
<?php else: // NO INSTANCE EXISTS ?>
<?php
$eligibility = $engine->checkEligibility($person_id, $process_definition_id);
?>
<div class="text-center">
<?php if ($eligibility['is_eligible']): ?>
<h4>Process Not Started</h4>
<p>This process has not been started for this person.</p>
<button id="startProcessBtn" class="btn btn-primary" data-person-id="<?= $person_id ?>" data-process-id="<?= $process_definition_id ?>">
Start Process
</button>
<?php else: ?>
<h4>Not Eligible</h4>
<p>This person is not eligible to start this process.</p>
<ul class="list-group list-group-flush text-start">
<?php foreach ($eligibility['reasons'] as $reason): ?>
<li class="list-group-item list-group-item-danger"><?= htmlspecialchars($reason) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<?php endif; ?>

View File

@ -2,48 +2,96 @@
require_once 'db/config.php';
require_once 'lib/ErrorHandler.php';
register_error_handler();
session_start();
function validate_definition_json($json) {
if (empty($json)) {
return; // No validation for empty json
}
$data = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(422);
throw new WorkflowRuleFailedException('Invalid JSON format in definition.');
}
$allowed_statuses = ['none', 'negative', 'in_progress', 'positive'];
if (isset($data['nodes'])) {
foreach ($data['nodes'] as $node) {
if (isset($node['ui_hints']['status']) && !in_array($node['ui_hints']['status'], $allowed_statuses)) {
http_response_code(422);
throw new WorkflowRuleFailedException('Invalid status in ui_hints. Allowed values are: ' . implode(', ', $allowed_statuses));
}
}
}
if (isset($data['transitions'])) {
foreach ($data['transitions'] as $transition) {
if (isset($transition['actions'])) {
foreach ($transition['actions'] as $action) {
if ($action['type'] === 'start_process' && isset($action['process_name'])) {
http_response_code(422);
throw new WorkflowRuleFailedException('Use process_code instead of process_name in transition actions.');
}
}
}
}
}
if (isset($data['eligibility_rules'])) {
foreach ($data['eligibility_rules'] as $rule) {
if ($rule['type'] === 'process_completed' && isset($rule['process_name'])) {
http_response_code(422);
throw new WorkflowRuleFailedException('Use process_code instead of process_name in eligibility_rules.');
}
}
}
}
try {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$processId = $_POST['process_id'] ?? null;
$name = $_POST['name'] ?? '';
$definition_json = $_POST['definition_json'] ?? '';
validate_definition_json($definition_json);
// Generate a simple code from the name
$code = strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $name)));
if (empty($name)) {
throw new WorkflowRuleFailedException('Process name is required.');
}
// Validate JSON
if (!empty($definition_json)) {
json_decode($definition_json);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new WorkflowRuleFailedException('Invalid JSON format in definition.');
}
}
$pdo = db();
if (empty($processId)) {
// Create new process
$sql = 'INSERT INTO process_definitions (name, definition_json, is_active) VALUES (?, ?, 1)';
$params = [$name, $definition_json, 1];
$sql = 'INSERT INTO process_definitions (name, code, definition_json, is_active) VALUES (?, ?, ?, 1)';
$params = [$name, $code, $definition_json];
$message = 'Process created successfully.';
} else {
// Update existing process
$sql = 'UPDATE process_definitions SET name = ?, definition_json = ? WHERE id = ?';
$params = [$name, $definition_json, $processId];
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 0;
$sql = 'UPDATE process_definitions SET name = ?, code = ?, definition_json = ?, is_active = ? WHERE id = ?';
$params = [$name, $code, $definition_json, $is_active, $processId];
$message = 'Process updated successfully.';
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) {
header('Content-Type: application/json');
echo json_encode(['message' => $message]);
} else {
$_SESSION['success_message'] = $message;
header('Location: process_definitions.php');
}
exit();
}
}
} catch (WorkflowRuleFailedException $e) {
header('Content-Type: application/json');
echo json_encode(['error' => $e->getMessage()]);
}

View File

@ -206,36 +206,50 @@ $status_colors = [
$instance = $instances[$person['id']][$process['id']] ?? null;
$lastActivity = $instance && isset($instance['last_activity_at']) ? date('d/m/y', strtotime($instance['last_activity_at'])) : '';
$is_eligible = $instance ? ($instance['is_eligible'] ?? true) : false;
// Correctly check eligibility using the WorkflowEngine
$eligibilityCheck = $workflowEngine->checkEligibility($person['id'], $process['id']);
$is_eligible = $eligibilityCheck['is_eligible'];
if ($instance && isset($instance['id'])) { // Existing instance
$is_active = $process['is_active'] ?? true;
$modal_target = ''; // Default to not clickable
$is_clickable = false;
if (!$is_active) {
$status = 'inactive';
$color = $status_colors['inactive'];
$title = 'Process inactive';
} elseif ($instance && isset($instance['id'])) { // Existing instance
$status = $instance['computed_status'];
$color = $status_colors[$status] ?? $status_colors['inactive'];
$title = ucfirst($status);
if (!empty($instance['computed_reason'])) {
$title = $instance['computed_reason'];
}
$title = !empty($instance['computed_reason']) ? $instance['computed_reason'] : ucfirst($status);
$modal_target = '#instanceModal';
$is_clickable = true;
} else { // No instance
if ($is_eligible) {
$status = 'not_started';
$color = $status_colors[$status];
$title = 'Not Started';
$modal_target = '#bulkInitModal';
$modal_target = '#instanceModal';
$is_clickable = true;
} else {
$status = 'ineligible';
$color = '#e9ecef'; // A light gray color
$title = 'Not eligible';
$modal_target = ''; // Prevent modal
$color = '#e9ecef'; // A light gray color for the circle
$title = implode(' ', $eligibilityCheck['reasons']); // Use the reason from the engine
$modal_target = '#instanceModal'; // Still open the modal to show details
$is_clickable = true;
}
}
?>
<td class="text-center align-middle"
style="cursor: <?= $modal_target ? 'pointer' : 'not-allowed' ?>;"
<?= $modal_target ? 'data-bs-toggle="modal"' : '' ?>
<?php if ($is_clickable): ?>
style="cursor: pointer;"
data-bs-toggle="modal"
data-bs-target="<?= $modal_target ?>"
data-person-id="<?= $person['id'] ?>"
data-process-id="<?= $process['id'] ?>"
<?php else: ?>
style="cursor: not-allowed;"
<?php endif; ?>
title="<?= htmlspecialchars($title) ?>">
<span style="height: 20px; width: 20px; background-color: <?= $color ?>; border-radius: 50%; display: inline-block;"></span>
<small class="text-muted d-block mt-1"><?= $lastActivity ?></small>