From 3b1a26adc9a636757f4ae1252c3a470bf39a2528 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 10 Jan 2026 20:46:53 +0000 Subject: [PATCH] =?UTF-8?q?G=C5=82=C4=99boka=20refactoryzacja=20kodu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WorkflowEngine.php | 138 ++++++++++++----- _get_instance_details.php | 285 +++++++++++++++++++---------------- _save_process_definition.php | 118 ++++++++++----- index.php | 46 ++++-- 4 files changed, 372 insertions(+), 215 deletions(-) diff --git a/WorkflowEngine.php b/WorkflowEngine.php index f963cf2..16f74e5 100644 --- a/WorkflowEngine.php +++ b/WorkflowEngine.php @@ -1,6 +1,7 @@ $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) { - switch ($rule['type']) { - case 'process_completed': - $this->checkProcessCompletedRule($personId, $rule); - break; - // Add other rule types here + foreach ($definition['eligibility_rules'] as $rule) { + try { + $params = $rule['params'] ?? $rule; + switch ($rule['type']) { + case 'checkProcessCompletedRule': + case 'process_completed': // Backward compatibility + $this->checkProcessCompletedRule($personId, $params); + break; + case 'checkProcessDataRule': + $this->checkProcessDataRule($personId, $params); + break; + // Add other rule types here + } + } catch (WorkflowNotAllowedException $e) { + $reasons[] = $e->getMessage(); + } + } + + 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("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 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']]); - $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']}'."); - } - } - 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]); + } } \ No newline at end of file diff --git a/_get_instance_details.php b/_get_instance_details.php index eaa211b..07a6dec 100644 --- a/_get_instance_details.php +++ b/_get_instance_details.php @@ -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,140 +33,172 @@ $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 "

Could not find person or process.

"; + exit; +} + +// Try to find an existing instance +$instance = $engine->getInstanceByDefId($person_id, $process_definition_id); ?>
- - + -
- + getEvents($instanceId); ?> -
-
Zadania do wykonania
-
- -
- > - -
- -
-
- - getProcessDefinitionNodes($process_definition_id); - $availableTransitions = $engine->getAvailableTransitions($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; - } - - $visited_nodes = []; - foreach ($events as $event) { - if ($event['node_id']) { - $visited_nodes[$event['node_id']] = true; - } - } - ?> -
-
Kroki procesu
- -
- - -
- -
-
Dodaj notatkę
-
- -
- -
- -
- -
-
Historia
- -

Brak zdarzeń.

+ +
+ - +
+
Available Actions
+ +

No actions available.

+ + + + + +
+ - +
+
+
Dodaj notatkę
+
+ +
+ +
+
+ +
+
Historia
+ +

Brak zdarzeń.

+ + + +
+ + + checkEligibility($person_id, $process_definition_id); + ?> + +
+ +

Process Not Started

+

This process has not been started for this person.

+ + +

Not Eligible

+

This person is not eligible to start this process.

+ + +
+ \ No newline at end of file diff --git a/_save_process_definition.php b/_save_process_definition.php index 8fb7822..a956073 100644 --- a/_save_process_definition.php +++ b/_save_process_definition.php @@ -2,48 +2,96 @@ require_once 'db/config.php'; require_once 'lib/ErrorHandler.php'; +register_error_handler(); + session_start(); -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $processId = $_POST['process_id'] ?? null; - $name = $_POST['name'] ?? ''; - $definition_json = $_POST['definition_json'] ?? ''; - - if (empty($name)) { - throw new WorkflowRuleFailedException('Process name is required.'); +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.'); } - // 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.'); + $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)); + } } } - $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]; - $message = 'Process created successfully.'; - } else { - // Update existing process - $sql = 'UPDATE process_definitions SET name = ?, definition_json = ? WHERE id = ?'; - $params = [$name, $definition_json, $processId]; - $message = 'Process updated successfully.'; + 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.'); + } + } + } + } } - - $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'); + + 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.'); + } + } } - exit(); } + +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.'); + } + + $pdo = db(); + + if (empty($processId)) { + // Create new process + $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 + $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()]); +} \ No newline at end of file diff --git a/index.php b/index.php index 0587d7f..6fc68cb 100644 --- a/index.php +++ b/index.php @@ -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']; + + $is_active = $process['is_active'] ?? true; + $modal_target = ''; // Default to not clickable + $is_clickable = false; - if ($instance && isset($instance['id'])) { // Existing instance + 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; } } ?> - - data-bs-target="" - data-person-id="" - data-process-id="" + + style="cursor: pointer;" + data-bs-toggle="modal" + data-bs-target="" + data-person-id="" + data-process-id="" + + style="cursor: not-allowed;" + title="">