From a703aeb1e238ebd84324af7ca379b47ecd160359 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 10 Jan 2026 19:52:03 +0000 Subject: [PATCH] =?UTF-8?q?Wersja=20po=20posprz=C4=85taniu=20kodu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WorkflowEngine.php | 343 +++++++++++++++--- _add_process_event.php | 35 +- _apply_transition.php | 60 +-- _bulk_add_event.php | 59 ++- _bulk_init_instances.php | 67 ++-- _bulk_update_status.php | 58 ++- _create_person.php | 10 +- _get_instance_details.php | 32 +- _get_process_bulk_details.php | 12 +- _header.php | 2 + _init_instances.php | 8 +- _init_single_instance.php | 10 +- _save_process_definition.php | 52 +-- _update_calendar_event.php | 14 +- _update_instance_status.php | 31 +- _update_person.php | 10 +- _update_training_checklist_status.php | 73 +--- assets/js/process_definitions.js | 157 ++++++++ .../021_rename_camelcase_columns.php | 26 ++ ...2_add_is_active_to_process_definitions.php | 12 + db_setup.php | 30 +- index.php | 165 ++++----- lib/ErrorHandler.php | 49 +++ lib/WorkflowExceptions.php | 44 +++ login.php | 2 +- process_definitions.php | 106 +----- 26 files changed, 919 insertions(+), 548 deletions(-) create mode 100644 assets/js/process_definitions.js create mode 100644 db/migrations/021_rename_camelcase_columns.php create mode 100644 db/migrations/022_add_is_active_to_process_definitions.php create mode 100644 lib/ErrorHandler.php create mode 100644 lib/WorkflowExceptions.php diff --git a/WorkflowEngine.php b/WorkflowEngine.php index 98ed786..f963cf2 100644 --- a/WorkflowEngine.php +++ b/WorkflowEngine.php @@ -12,14 +12,29 @@ class WorkflowEngine { public function getDashboardMatrix(): array { // Get all people (potential assignees) - $stmt_people = $this->pdo->prepare("SELECT id, firstName, lastName, companyName, role, email, phone FROM people ORDER BY lastName, firstName"); + $stmt_people = $this->pdo->prepare(" + SELECT p.*, bg.name as bni_group_name + FROM people p + LEFT JOIN bni_groups bg ON p.bni_group_id = bg.id + ORDER BY p.last_name, p.first_name + "); $stmt_people->execute(); $people = $stmt_people->fetchAll(PDO::FETCH_ASSOC); - // Fetch all process definitions - $stmt_defs = $this->pdo->prepare("SELECT id, name FROM process_definitions ORDER BY name"); + // Fetch all process definitions with their JSON + $stmt_defs = $this->pdo->prepare("SELECT id, name, definition_json FROM process_definitions ORDER BY name"); $stmt_defs->execute(); - $process_definitions = $stmt_defs->fetchAll(PDO::FETCH_ASSOC); + $process_definitions_raw = $stmt_defs->fetchAll(PDO::FETCH_ASSOC); + + $definitions = []; + $definition_map = []; + foreach ($process_definitions_raw as $def) { + $definitions[$def['id']] = [ + 'id' => $def['id'], + 'name' => $def['name'] + ]; + $definition_map[$def['id']] = !empty($def['definition_json']) ? json_decode($def['definition_json'], true) : null; + } // Fetch instances $stmt_instances = $this->pdo->prepare("SELECT * FROM process_instances"); @@ -28,21 +43,94 @@ class WorkflowEngine { $instances = []; foreach ($instances_data as $instance) { - $instances[$instance['person_id']][$instance['process_definition_id']] = $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; } + 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]; + } + } + } + + // 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); + + return [ 'people' => $people, - 'definitions' => $process_definitions, + 'definitions' => array_values($definitions), 'instances' => $instances, + 'all_functions' => $all_functions, + 'person_functions_map' => $person_functions_map, + 'bni_groups' => $bni_groups, ]; } - public function startProcess(string $processCode, int $personId, int $userId): ?int { + public function startProcess(string $processCode, int $personId, int $userId): int { $this->pdo->beginTransaction(); try { // 1. Find active process definition by code. - $stmt_def = $this->pdo->prepare("SELECT * FROM process_definitions WHERE code = ? AND active = 1"); + $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); @@ -53,12 +141,12 @@ class WorkflowEngine { $definition = $stmt_def->fetch(PDO::FETCH_ASSOC); if (!$definition) { - throw new Exception("Process definition with code or id '$processCode' not found."); + throw new WorkflowNotFoundException("Process definition with code or id '$processCode' not found."); } $definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : []; if (empty($definition_json) || $definition_json['type'] !== 'checklist') { - throw new Exception("Process definition with code '$processCode' not found or not a checklist."); + throw new WorkflowNotAllowedException("Process definition with code '$processCode' not found or not a checklist."); } // For checklists, there's no start_node_id, so we can proceed with instance creation @@ -67,14 +155,14 @@ class WorkflowEngine { } else { $definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : []; if (empty($definition_json) || !isset($definition_json['start_node_id'])) { - throw new Exception("Process definition is missing start_node_id."); + throw new WorkflowRuleFailedException("Process definition is missing start_node_id."); } $startNodeId = $definition_json['start_node_id']; } // 2. Create a new process instance. $stmt_insert = $this->pdo->prepare( - "INSERT INTO process_instances (person_id, process_definition_id, current_node_id, current_status, lastActivityAt) VALUES (?, ?, ?, 'in_progress', NOW())" + "INSERT INTO process_instances (person_id, process_definition_id, current_node_id, current_status, last_activity_at) VALUES (?, ?, ?, 'in_progress', NOW())" ); $stmt_insert->execute([$personId, $definition['id'], $startNodeId]); $instanceId = $this->pdo->lastInsertId(); @@ -86,8 +174,7 @@ class WorkflowEngine { return (int)$instanceId; } catch (Exception $e) { $this->pdo->rollBack(); - error_log("Error in startProcess: " . $e->getMessage()); - return null; + throw $e; } } @@ -115,19 +202,18 @@ class WorkflowEngine { ]; } - public function applyTransition(int $instanceId, string $transitionId, array $inputPayload, int $userId): bool { + public function applyTransition(int $instanceId, string $transitionId, array $inputPayload, int $userId): array { $this->pdo->beginTransaction(); try { $state = $this->getProcessState($instanceId); if (!$state) { - throw new Exception("Process instance not found."); + throw new WorkflowNotFoundException("Process instance not found."); } $instance = $state['instance']; $definition = $state['definition']; $currentNodeId = $instance['current_node_id']; - // Find the transition from the definition $transition = null; foreach ($definition['transitions'] as $t) { if ($t['from'] === $currentNodeId && $t['id'] === $transitionId) { @@ -137,7 +223,7 @@ class WorkflowEngine { } if (!$transition) { - throw new Exception("Transition not found or not allowed from the current node."); + throw new WorkflowNotAllowedException("Transition not found or not allowed from the current node."); } // TODO: Add rule validation here @@ -145,37 +231,53 @@ class WorkflowEngine { $newNodeId = $transition['to']; $newNodeInfo = $definition['nodes'][$newNodeId] ?? null; - // Update instance + $newStatus = $newNodeInfo['ui_hints']['status'] ?? 'in_progress'; + $newReason = $newNodeInfo['ui_hints']['reason'] ?? ''; + $newNextStep = $newNodeInfo['ui_hints']['next_step'] ?? ''; + $stmt_update = $this->pdo->prepare( - "UPDATE process_instances SET current_node_id = ?, current_status = ?, current_reason = ?, suggested_next_step = ?, lastActivityAt = NOW() WHERE id = ?" + "UPDATE process_instances SET current_node_id = ?, current_status = ?, current_reason = ?, suggested_next_step = ?, last_activity_at = NOW() WHERE id = ?" ); $stmt_update->execute([ $newNodeId, - $newNodeInfo['ui_hints']['status'] ?? 'in_progress', - $newNodeInfo['ui_hints']['reason'] ?? '', - $newNodeInfo['ui_hints']['next_step'] ?? '', + $newStatus, + $newReason, + $newNextStep, $instanceId ]); - // Add event $message = $inputPayload['message'] ?? $transition['name']; $this->addEvent($instanceId, 'transition_applied', $message, $newNodeId, $inputPayload, $userId); + if (isset($transition['actions'])) { + foreach ($transition['actions'] as $action) { + if ($action['type'] === 'start_process') { + $this->executeStartProcessAction($instance['person_id'], $action, $userId); + } + } + } + $this->pdo->commit(); - return true; + + return [ + 'instanceId' => $instanceId, + 'currentNodeId' => $newNodeId, + 'currentStatus' => $newStatus, + 'currentReason' => $newReason, + 'suggestedNextStep' => $newNextStep, + 'lastActivityAt' => date('Y-m-d H:i:s'), + ]; } catch (Exception $e) { $this->pdo->rollBack(); - error_log("Error in applyTransition: " . $e->getMessage()); - return false; + // Re-throw the original exception to be handled by the global error handler + throw $e; } } public function addNote(int $instanceId, string $message, int $userId): bool { $state = $this->getProcessState($instanceId); if (!$state) { - // Even if the instance isn't found, we shouldn't crash. Log and return false. - error_log("addNote failed: Process instance $instanceId not found."); - return false; + throw new WorkflowNotFoundException("Process instance #$instanceId not found."); } $currentNodeId = $state['instance']['current_node_id']; $payload = ['message' => $message]; @@ -183,33 +285,145 @@ class WorkflowEngine { return true; } + public function applyManualStatus(int $instanceId, string $status, string $reasonOrNote, int $userId): bool { + $this->pdo->beginTransaction(); + try { + $state = $this->getProcessState($instanceId); + if (!$state) { + throw new WorkflowNotFoundException("Process instance #$instanceId not found."); + } + + $stmt_update = $this->pdo->prepare( + "UPDATE process_instances SET current_status = ?, current_reason = ?, last_activity_at = NOW() WHERE id = ?" + ); + $stmt_update->execute([$status, $reasonOrNote, $instanceId]); + + $currentNodeId = $state['instance']['current_node_id']; + $message = "Status manually set to '$status'."; + if (!empty($reasonOrNote)) { + $message .= " Reason: $reasonOrNote"; + } + + $this->addEvent($instanceId, 'manual_status_change', $message, $currentNodeId, ['status' => $status, 'reason' => $reasonOrNote], $userId); + + $this->pdo->commit(); + return true; + } catch (Exception $e) { + $this->pdo->rollBack(); + throw $e; + } + } + + public function bulkAddNotes(array $notes): array + { + $results = []; + foreach ($notes as $note) { + try { + $this->addNote((int)$note['instance_id'], $note['message'], (int)$note['user_id']); + $results[] = ['instance_id' => $note['instance_id'], 'success' => true]; + } catch (Exception $e) { + $results[] = ['instance_id' => $note['instance_id'], 'success' => false, 'error' => $e->getMessage()]; + } + } + return $results; + } + + public function bulkManualStatus(array $statuses): array + { + $results = []; + foreach ($statuses as $status) { + try { + $this->applyManualStatus((int)$status['instance_id'], $status['status'], $status['reason'] ?? '', (int)$status['user_id']); + $results[] = ['instance_id' => $status['instance_id'], 'success' => true]; + } catch (Exception $e) { + $results[] = ['instance_id' => $status['instance_id'], 'success' => false, 'error' => $e->getMessage()]; + } + } + return $results; + } + + public function updateChecklistStatus(int $instanceId, string $taskCode, bool $isChecked, int $userId): array + { + $this->pdo->beginTransaction(); + try { + // Get current data_json + $stmt = $this->pdo->prepare("SELECT data_json, process_definition_id FROM process_instances WHERE id = ?"); + $stmt->execute([$instanceId]); + $instance = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$instance) { + throw new WorkflowNotFoundException("Process instance #$instanceId not found."); + } + + $data = $instance['data_json'] ? json_decode($instance['data_json'], true) : []; + + // Update the specific task status + $data[$taskCode] = $isChecked; + $newDataJson = json_encode($data); + + // Save new data_json and update timestamp + $stmt = $this->pdo->prepare("UPDATE process_instances SET data_json = ?, last_activity_at = CURRENT_TIMESTAMP WHERE id = ?"); + $stmt->execute([$newDataJson, $instanceId]); + + // Add an event for the checklist update + $message = "Checklist task '$taskCode' marked as " . ($isChecked ? 'complete' : 'incomplete') . "."; + $this->addEvent($instanceId, 'checklist_update', $message, null, ['task' => $taskCode, 'checked' => $isChecked], $userId); + + // Calculate progress + $stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?"); + $stmt_def->execute([$instance['process_definition_id']]); + $definitionJson = $stmt_def->fetchColumn(); + $definition = json_decode($definitionJson, true); + $totalTasks = count($definition['tasks'] ?? []); + $completedTasks = count(array_filter($data)); + + $this->pdo->commit(); + + return [ + 'success' => true, + 'progress' => [ + 'completed' => $completedTasks, + 'total' => $totalTasks + ], + 'lastActivityAt' => date('d/m/y') + ]; + + } catch (Exception $e) { + $this->pdo->rollBack(); + throw $e; + } + } + + private function addEvent(int $instanceId, string $eventType, string $message, ?string $nodeId, array $payload, int $userId): void { $stmt = $this->pdo->prepare( - "INSERT INTO process_events (processInstanceId, event_type, message, node_id, payload_json, createdBy, createdAt) VALUES (?, ?, ?, ?, ?, ?, NOW())" + "INSERT INTO process_events (process_instance_id, event_type, message, node_id, payload_json, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())" ); $stmt->execute([$instanceId, $eventType, $message, $nodeId, json_encode($payload), $userId]); } - public function getOrCreateInstanceByDefId(int $personId, int $processDefinitionId, int $userId): ?array { + public function getOrCreateInstanceByDefId(int $personId, int $processDefinitionId, int $userId): ?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); if (!$instance) { - // For checklists, the process code is the process definition ID - $stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?"); + $stmt_def = $this->pdo->prepare("SELECT definition_json, code FROM process_definitions WHERE id = ?"); $stmt_def->execute([$processDefinitionId]); - $definition_json = $stmt_def->fetchColumn(); - $definition = !empty($definition_json) ? json_decode($definition_json, true) : []; + $definition = $stmt_def->fetch(PDO::FETCH_ASSOC); - if ($definition && isset($definition['type']) && $definition['type'] === 'checklist') { - $processCode = (string) $processDefinitionId; - } else { - $stmt_def = $this->pdo->prepare("SELECT code FROM process_definitions WHERE id = ?"); - $stmt_def->execute([$processDefinitionId]); - $processCode = $stmt_def->fetchColumn(); + if (!$definition) { + throw new WorkflowNotFoundException("Process definition #$processDefinitionId not found."); } + $this->checkEligibility($personId, $definition); + + $definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : []; + + $processCode = ($definition_json && isset($definition_json['type']) && $definition_json['type'] === 'checklist') + ? (string) $processDefinitionId + : $definition['code']; + if($processCode) { $instanceId = $this->startProcess($processCode, $personId, $userId); if($instanceId) { @@ -219,11 +433,11 @@ class WorkflowEngine { } } - return $instance !== false ? $instance : null; + return $instance ?: null; } public function getEvents(int $instanceId): array { - $stmt_events = $this->pdo->prepare("SELECT pe.*, p.email as user_email, p.firstName, p.lastName FROM process_events pe JOIN people p ON pe.createdBy = p.id WHERE pe.processInstanceId = ? ORDER BY pe.createdAt DESC"); + $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]); return $stmt_events->fetchAll(PDO::FETCH_ASSOC); } @@ -261,4 +475,45 @@ class WorkflowEngine { $definition = !empty($json) ? json_decode($json, true) : []; 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 + } + + foreach ($definition_json['eligibility_rules'] as $rule) { + switch ($rule['type']) { + case 'process_completed': + $this->checkProcessCompletedRule($personId, $rule); + break; + // Add other rule types here + } + } + } + + 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']]); + $processDefinitionId = $stmt->fetchColumn(); + + if ($processDefinitionId) { + $this->getOrCreateInstanceByDefId($personId, $processDefinitionId, $userId); + } + } } \ No newline at end of file diff --git a/_add_process_event.php b/_add_process_event.php index 14344a4..5b643dc 100644 --- a/_add_process_event.php +++ b/_add_process_event.php @@ -1,31 +1,28 @@ prepare("INSERT INTO process_events (processInstanceId, eventType, description, createdBy) VALUES (?, ?, ?, ?)"); -$stmt->execute([$instanceId, $eventType, $description, $userId]); +$workflowEngine = new WorkflowEngine(); +$workflowEngine->addNote((int)$instanceId, $message, (int)$userId); -// Update the last activity time for the instance -$stmt_update = $pdo->prepare("UPDATE process_instances SET lastActivityAt = NOW() WHERE id = ?"); -$stmt_update->execute([$instanceId]); - -// Redirect back to the dashboard -header("Location: process_dashboard.php"); +if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) { + header('Content-Type: application/json'); + echo json_encode(['message' => 'Note added successfully.']); +} else { + header('Location: ' . $_SERVER['HTTP_REFERER']); +} exit; diff --git a/_apply_transition.php b/_apply_transition.php index fced8b8..d928959 100644 --- a/_apply_transition.php +++ b/_apply_transition.php @@ -1,23 +1,18 @@ false, 'message' => 'Wystąpił nieoczekiwany błąd.']; - if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - http_response_code(405); - $response['message'] = 'Invalid request method.'; - echo json_encode($response); - exit; + throw new WorkflowNotAllowedException('Invalid request method.'); } require_once 'WorkflowEngine.php'; if (!isset($_POST['instanceId']) || !isset($_POST['transitionId'])) { - http_response_code(400); - $response['message'] = 'Błąd: Brak wymaganych parametrów.'; - echo json_encode($response); - exit; + throw new WorkflowNotAllowedException('Błąd: Brak wymaganych parametrów.'); } $instanceId = (int)$_POST['instanceId']; @@ -26,42 +21,21 @@ $userId = $_SESSION['user_id'] ?? null; $payload = $_POST['payload'] ?? []; if (!$userId) { - http_response_code(401); - $response['message'] = 'Błąd: Sesja wygasła.'; - echo json_encode($response); - exit; + throw new WorkflowNotAllowedException('Błąd: Sesja wygasła.', [], 401); } -try { - $engine = new WorkflowEngine(); - $success = false; +$engine = new WorkflowEngine(); - if ($transitionId === 'note') { - // Special case: Just add a note, don't change state. - $message = $payload['message'] ?? ''; - if (!empty($message)) { - $success = $engine->addNote($instanceId, $message, $userId); - if ($success) { - $response['message'] = 'Notatka została dodana.'; - } - } - } else { - // Standard transition logic - $success = $engine->applyTransition($instanceId, $transitionId, $payload, $userId); - if ($success) { - $response['message'] = 'Akcja została wykonana pomyślnie.'; - } +if ($transitionId === 'note') { + $message = $payload['message'] ?? ''; + if (empty($message)) { + throw new WorkflowNotAllowedException('Treść notatki nie może być pusta.'); } - - if ($success) { - $response['success'] = true; - } - -} catch (Exception $e) { - error_log("Error applying transition: " . $e->getMessage()); - http_response_code(500); - $response['message'] = 'Wystąpił krytyczny błąd: ' . $e->getMessage(); + $engine->addNote($instanceId, $message, $userId); + $response = ['success' => true, 'message' => 'Notatka została dodana.']; +} else { + $result = $engine->applyTransition($instanceId, $transitionId, $payload, $userId); + $response = ['success' => true, 'message' => 'Akcja została wykonana pomyślnie.', 'data' => $result]; } -echo json_encode($response); -exit; \ No newline at end of file +echo json_encode($response); \ No newline at end of file diff --git a/_bulk_add_event.php b/_bulk_add_event.php index 6c554a7..1008fac 100644 --- a/_bulk_add_event.php +++ b/_bulk_add_event.php @@ -1,53 +1,50 @@ prepare("SELECT id FROM process_instances WHERE processDefinitionId = ? AND personId IN ($placeholders)"); +$stmt = $pdo->prepare("SELECT id FROM process_instances WHERE process_definition_id = ? AND person_id IN ($placeholders)"); $params = array_merge([$process_id], $person_ids); $stmt->execute($params); $instance_ids = $stmt->fetchAll(PDO::FETCH_COLUMN); -if (!empty($instance_ids)) { - $instance_placeholders = implode(',', array_fill(0, count($instance_ids), '?')); - - // Update last activity - $stmt_update = $pdo->prepare("UPDATE process_instances SET lastActivityAt = NOW() WHERE id IN ($instance_placeholders)"); - $stmt_update->execute($instance_ids); - - // Bulk insert events - $event_sql = "INSERT INTO process_events (processInstanceId, event_type, message, createdById) VALUES "; - $event_rows = []; - $event_params = []; - foreach($instance_ids as $instance_id) { - $event_rows[] = "(?, 'note', ?, ?)"; - $event_params[] = $instance_id; - $event_params[] = $message; - $event_params[] = $userId; - } - $event_sql .= implode(', ', $event_rows); - $stmt_event = $pdo->prepare($event_sql); - $stmt_event->execute($event_params); +if (empty($instance_ids)) { + $_SESSION['flash_message'] = "No instances found for the selected people and process."; + header('Location: ' . $_SERVER['HTTP_REFERER']); + exit; } -$_SESSION['flash_message'] = "Bulk event addition completed."; -header('Location: process_dashboard.php'); +$notes = []; +foreach ($instance_ids as $instance_id) { + $notes[] = [ + 'instance_id' => $instance_id, + 'message' => $message, + 'user_id' => $userId + ]; +} + +$workflowEngine = new WorkflowEngine(); +$results = $workflowEngine->bulkAddNotes($notes); + +$_SESSION['flash_message'] = "Bulk note addition completed."; +$_SESSION['bulk_results'] = $results; + +header('Location: ' . $_SERVER['HTTP_REFERER']); exit; diff --git a/_bulk_init_instances.php b/_bulk_init_instances.php index fda7a2e..f3f8274 100644 --- a/_bulk_init_instances.php +++ b/_bulk_init_instances.php @@ -1,33 +1,56 @@ prepare($sql); -$stmt->execute($params); +$engine = new WorkflowEngine(); +$results = [ + 'success' => [], + 'failed' => [], +]; -$_SESSION['flash_message'] = "Bulk initialization completed."; -header('Location: process_dashboard.php'); -exit; +foreach ($personIds as $personId) { + try { + $instance = $engine->getOrCreateInstanceByDefId($personId, $process_id, $userId); + if ($instance) { + $results['success'][] = $personId; + } else { + $results['failed'][] = $personId; + } + } catch (Exception $e) { + $results['failed'][] = $personId; + // Optionally log the error + error_log("Failed to initialize process for person $personId: " . $e->getMessage()); + } +} + +$message = "Bulk initialization completed. Success: " . count($results['success']) . ", Failed: " . count($results['failed']); + +if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) { + header('Content-Type: application/json'); + echo json_encode([ + 'message' => $message, + 'results' => $results + ]); +} else { + $_SESSION['success_message'] = $message; + header('Location: index.php'); +} +exit(); \ No newline at end of file diff --git a/_bulk_update_status.php b/_bulk_update_status.php index 060be43..265610c 100644 --- a/_bulk_update_status.php +++ b/_bulk_update_status.php @@ -1,54 +1,52 @@ prepare("SELECT id FROM process_instances WHERE processDefinitionId = ? AND personId IN ($placeholders)"); +$stmt = $pdo->prepare("SELECT id FROM process_instances WHERE process_definition_id = ? AND person_id IN ($placeholders)"); $params = array_merge([$process_id], $person_ids); $stmt->execute($params); $instance_ids = $stmt->fetchAll(PDO::FETCH_COLUMN); -if (!empty($instance_ids)) { - $instance_placeholders = implode(',', array_fill(0, count($instance_ids), '?')); - - // Update statuses - $stmt_update = $pdo->prepare("UPDATE process_instances SET current_status = ?, lastActivityAt = NOW() WHERE id IN ($instance_placeholders)"); - $stmt_update->execute(array_merge([$status], $instance_ids)); - - // Bulk insert events - $event_sql = "INSERT INTO process_events (processInstanceId, event_type, message, createdById) VALUES "; - $event_rows = []; - $event_params = []; - $message = "Status changed to $status"; - foreach($instance_ids as $instance_id) { - $event_rows[] = "(?, 'status_change', ?, ?)"; - $event_params[] = $instance_id; - $event_params[] = $message; - $event_params[] = $userId; - } - $event_sql .= implode(', ', $event_rows); - $stmt_event = $pdo->prepare($event_sql); - $stmt_event->execute($event_params); +if (empty($instance_ids)) { + $_SESSION['flash_message'] = "No instances found for the selected people and process."; + header('Location: ' . $_SERVER['HTTP_REFERER']); + exit; } +$statuses = []; +foreach ($instance_ids as $instance_id) { + $statuses[] = [ + 'instance_id' => $instance_id, + 'status' => $status, + 'reason' => $reason, + 'user_id' => $userId + ]; +} + +$workflowEngine = new WorkflowEngine(); +$results = $workflowEngine->bulkManualStatus($statuses); + $_SESSION['flash_message'] = "Bulk status update completed."; -header('Location: process_dashboard.php'); +$_SESSION['bulk_results'] = $results; + +header('Location: ' . $_SERVER['HTTP_REFERER']); exit; diff --git a/_create_person.php b/_create_person.php index 30dd785..df36c3b 100644 --- a/_create_person.php +++ b/_create_person.php @@ -3,11 +3,11 @@ require_once 'db/config.php'; session_start(); if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $firstName = $_POST['firstName']; - $lastName = $_POST['lastName']; + $first_name = $_POST['first_name']; + $last_name = $_POST['last_name']; $email = $_POST['email']; $password = $_POST['password']; - $companyName = $_POST['companyName'] ?? null; + $company_name = $_POST['company_name'] ?? null; $phone = $_POST['phone'] ?? null; $role = $_POST['role'] ?? 'guest'; $functions = isset($_POST['functions']) ? $_POST['functions'] : []; @@ -35,9 +35,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $pdo->beginTransaction(); // Insert person details first - $sql = 'INSERT INTO people (firstName, lastName, email, password, companyName, phone, role, bni_group_id, nip, industry, company_size_revenue, business_description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + $sql = 'INSERT INTO people (first_name, last_name, email, password, company_name, phone, role, bni_group_id, nip, industry, company_size_revenue, business_description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; $stmt = $pdo->prepare($sql); - $stmt->execute([$firstName, $lastName, $email, password_hash($password, PASSWORD_DEFAULT), $companyName, $phone, $role, $bni_group_id, $nip, $industry, $company_size_revenue, $business_description]); + $stmt->execute([$first_name, $last_name, $email, password_hash($password, PASSWORD_DEFAULT), $company_name, $phone, $role, $bni_group_id, $nip, $industry, $company_size_revenue, $business_description]); $personId = $pdo->lastInsertId(); // Handle file uploads now that we have a personId diff --git a/_get_instance_details.php b/_get_instance_details.php index 514a6e8..eaa211b 100644 --- a/_get_instance_details.php +++ b/_get_instance_details.php @@ -1,19 +1,20 @@ getOrCreateInstanceByDefId($personId, $processDefinitionId, $userId); +$instance = $engine->getOrCreateInstanceByDefId($person_id, $process_definition_id, $userId); if (!$instance) { - http_response_code(500); - die("Nie można pobrać lub utworzyć instancji procesu."); + throw new WorkflowNotFoundException("Nie można pobrać lub utworzyć instancji procesu."); } $instanceId = $instance['id']; // 2. Fetch all related data -$stmt_person = $pdo->prepare("SELECT firstName, lastName FROM people WHERE id = ?"); -$stmt_person->execute([$personId]); +$stmt_person = $pdo->prepare("SELECT first_name, last_name FROM people WHERE id = ?"); +$stmt_person->execute([$person_id]); $person = $stmt_person->fetch(); $stmt_process = $pdo->prepare("SELECT * FROM process_definitions WHERE id = ?"); -$stmt_process->execute([$processDefinitionId]); +$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'); @@ -45,7 +45,7 @@ $events = $engine->getEvents($instanceId);
- - + -
@@ -73,7 +73,7 @@ $events = $engine->getEvents($instanceId); getProcessDefinitionNodes($processDefinitionId); + $all_nodes = $engine->getProcessDefinitionNodes($process_definition_id); $availableTransitions = $engine->getAvailableTransitions($instanceId); $available_target_node_ids = array_map(function($t) { return $t['to']; }, $availableTransitions); @@ -164,7 +164,7 @@ HTML; echo '

' . htmlspecialchars($message) . '

'; } ?> - Przez dnia + Przez dnia diff --git a/_get_process_bulk_details.php b/_get_process_bulk_details.php index 05ed8e5..580e1d3 100644 --- a/_get_process_bulk_details.php +++ b/_get_process_bulk_details.php @@ -35,7 +35,7 @@ try { } // 2. Get all instances for this process - $stmt_instances = $pdo->prepare("SELECT * FROM process_instances WHERE processDefinitionId = ?"); + $stmt_instances = $pdo->prepare("SELECT * FROM process_instances WHERE process_definition_id = ?"); $stmt_instances->execute([$process_id]); $instances = $stmt_instances->fetchAll(PDO::FETCH_ASSOC); @@ -45,29 +45,29 @@ try { $events = []; if (!empty($instance_ids)) { $placeholders = implode(',', array_fill(0, count($instance_ids), '?')); - $stmt_events = $pdo->prepare("SELECT * FROM process_events WHERE processInstanceId IN ($placeholders) ORDER BY createdAt, id"); + $stmt_events = $pdo->prepare("SELECT * FROM process_events WHERE process_instance_id IN ($placeholders) ORDER BY created_at, id"); $stmt_events->execute($instance_ids); $all_events = $stmt_events->fetchAll(PDO::FETCH_ASSOC); // Group events by instance_id foreach ($all_events as $event) { - $events[$event['processInstanceId']][] = $event; + $events[$event['process_instance_id']][] = $event; } } // 4. Get People details - $people_ids = array_unique(array_column($instances, 'personId')); + $people_ids = array_unique(array_column($instances, 'person_id')); $people = []; if (!empty($people_ids)) { $valid_people_ids = array_filter($people_ids, 'is_numeric'); if (!empty($valid_people_ids)) { $placeholders = implode(',', array_fill(0, count($valid_people_ids), '?')); - $stmt_people = $pdo->prepare("SELECT id, firstName, lastName FROM people WHERE id IN ($placeholders)"); + $stmt_people = $pdo->prepare("SELECT id, first_name, last_name FROM people WHERE id IN ($placeholders)"); $stmt_people->execute(array_values($valid_people_ids)); $people_results = $stmt_people->fetchAll(PDO::FETCH_ASSOC); foreach ($people_results as $person) { $people[$person['id']] = $person; - $people[$person['id']]['name'] = trim($person['firstName'] . ' ' . $person['lastName']); + $people[$person['id']]['name'] = trim($person['first_name'] . ' ' . $person['last_name']); } } } diff --git a/_header.php b/_header.php index fc22757..5f72031 100644 --- a/_header.php +++ b/_header.php @@ -1,5 +1,7 @@ prepare("SELECT id FROM process_definitions WHERE is_act $stmt_processes->execute(); $processes = $stmt_processes->fetchAll(PDO::FETCH_COLUMN); -$insert_stmt = $pdo->prepare("INSERT IGNORE INTO process_instances (personId, processDefinitionId, current_status) VALUES (?, ?, 'none')"); +$insert_stmt = $pdo->prepare("INSERT IGNORE INTO process_instances (person_id, process_definition_id, current_status) VALUES (?, ?, 'none')"); $count = 0; -foreach ($people as $personId) { - foreach ($processes as $processId) { - $insert_stmt->execute([$personId, $processId]); +foreach ($people as $person_id) { + foreach ($processes as $process_id) { + $insert_stmt->execute([$person_id, $process_id]); if ($insert_stmt->rowCount() > 0) { $count++; } diff --git a/_init_single_instance.php b/_init_single_instance.php index 5e612c2..60b5c26 100644 --- a/_init_single_instance.php +++ b/_init_single_instance.php @@ -1,11 +1,11 @@ prepare($sql); - $stmt->execute($params); + $stmt = $pdo->prepare($sql); + $stmt->execute($params); - session_start(); + 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; - - } catch (PDOException $e) { - error_log('Save process definition failed: ' . $e->getMessage()); - die('Save process definition failed. Please check the logs.'); + header('Location: process_definitions.php'); } - - header('Location: process_definitions.php'); exit(); } diff --git a/_update_calendar_event.php b/_update_calendar_event.php index 67018fa..68216f8 100644 --- a/_update_calendar_event.php +++ b/_update_calendar_event.php @@ -1,13 +1,13 @@ rollBack(); error_log("Error updating event: " . $e->getMessage()); - header("Location: calendar.php?error=db_error"); - exit(); + throw $e; } } diff --git a/_update_instance_status.php b/_update_instance_status.php index 0f778ce..7b78c15 100644 --- a/_update_instance_status.php +++ b/_update_instance_status.php @@ -1,30 +1,29 @@ prepare("UPDATE process_instances SET status = ?, lastActivityAt = NOW() WHERE id = ?"); -$stmt->execute([$status, $instanceId]); +$workflowEngine = new WorkflowEngine(); +$workflowEngine->applyManualStatus((int)$instanceId, $status, $reason, (int)$userId); -// Create a status change event -$stmt_event = $pdo->prepare("INSERT INTO process_events (processInstanceId, eventType, description, createdBy) VALUES (?, 'status_change', ?, ?)"); -$stmt_event->execute([$instanceId, "Status changed to $status", $userId]); - -// Redirect back to the dashboard -header("Location: process_dashboard.php"); +if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) { + header('Content-Type: application/json'); + echo json_encode(['message' => 'Status updated successfully.']); +} else { + header('Location: index.php'); +} exit; diff --git a/_update_person.php b/_update_person.php index a7f3cfc..a8b7a3a 100644 --- a/_update_person.php +++ b/_update_person.php @@ -4,10 +4,10 @@ session_start(); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $personId = $_POST['id']; - $firstName = $_POST['firstName']; - $lastName = $_POST['lastName']; + $first_name = $_POST['first_name']; + $last_name = $_POST['last_name']; $email = $_POST['email']; - $companyName = $_POST['companyName']; + $company_name = $_POST['company_name']; $phone = $_POST['phone']; $role = $_POST['role'] ?? 'guest'; $functions = isset($_POST['functions']) ? $_POST['functions'] : []; @@ -61,12 +61,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Prepare SQL for updating person details $sql_parts = [ - 'firstName = ?', 'lastName = ?', 'email = ?', 'companyName = ?', 'phone = ?', + 'first_name = ?', 'last_name = ?', 'email = ?', 'company_name = ?', 'phone = ?', 'role = ?', 'bni_group_id = ?', 'nip = ?', 'industry = ?', 'company_size_revenue = ?', 'business_description = ?' ]; $params = [ - $firstName, $lastName, $email, $companyName, $phone, $role, $bni_group_id, + $first_name, $last_name, $email, $company_name, $phone, $role, $bni_group_id, $nip, $industry, $company_size_revenue, $business_description ]; diff --git a/_update_training_checklist_status.php b/_update_training_checklist_status.php index e8c6852..7191fd2 100644 --- a/_update_training_checklist_status.php +++ b/_update_training_checklist_status.php @@ -1,81 +1,36 @@ false, 'error' => 'Unauthorized']); - exit; + throw new WorkflowNotAllowedException('Unauthorized'); } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - http_response_code(405); - echo json_encode(['error' => 'Method Not Allowed']); - exit; + throw new WorkflowNotAllowedException('Method Not Allowed'); } $inputJSON = file_get_contents('php://input'); $input = json_decode($inputJSON, true); if (json_last_error() !== JSON_ERROR_NONE) { - http_response_code(400); - echo json_encode(['error' => 'Invalid JSON']); - exit; + throw new WorkflowRuleFailedException('Invalid JSON'); } $instanceId = $input['instance_id'] ?? null; $taskCode = $input['task_code'] ?? null; $isChecked = $input['is_checked'] ?? null; +$userId = $_SESSION['user_id']; if (!$instanceId || !$taskCode || $isChecked === null) { - http_response_code(400); - echo json_encode(['error' => 'Missing required parameters: instance_id, task_code, is_checked']); - exit; + throw new WorkflowRuleFailedException('Missing required parameters: instance_id, task_code, is_checked'); } -try { - $pdo = db(); - - // Get current data_json - $stmt = $pdo->prepare("SELECT data_json FROM process_instances WHERE id = ?"); - $stmt->execute([$instanceId]); - $currentDataJson = $stmt->fetchColumn(); - - $data = $currentDataJson ? json_decode($currentDataJson, true) : []; - - // Update the specific task status - $data[$taskCode] = (bool)$isChecked; - $newDataJson = json_encode($data); - - // Save new data_json and update timestamp - $stmt = $pdo->prepare("UPDATE process_instances SET data_json = ?, lastActivityAt = CURRENT_TIMESTAMP WHERE id = ?"); - $success = $stmt->execute([$newDataJson, $instanceId]); - - if ($success) { - // Calculate progress - $stmt = $pdo->prepare("SELECT pd.definition_json FROM process_definitions pd JOIN process_instances pi ON pd.id = pi.process_definition_id WHERE pi.id = ?"); - $stmt->execute([$instanceId]); - $definitionJson = $stmt->fetchColumn(); - $definition = json_decode($definitionJson, true); - $totalTasks = count($definition['tasks'] ?? []); - $completedTasks = count(array_filter($data)); - - echo json_encode([ - 'success' => true, - 'message' => 'Status updated successfully.', - 'progress' => [ - 'completed' => $completedTasks, - 'total' => $totalTasks - ], - 'lastActivityAt' => date('d/m/y') - ]); - } else { - http_response_code(500); - echo json_encode(['error' => 'Failed to update status.']); - } - -} catch (PDOException $e) { - http_response_code(500); - echo json_encode(['error' => 'Database error: ' . $e->getMessage()]); -} -?> \ No newline at end of file +$workflowEngine = new WorkflowEngine(); +$result = $workflowEngine->updateChecklistStatus((int)$instanceId, $taskCode, (bool)$isChecked, (int)$userId); +echo json_encode($result); +exit; \ No newline at end of file diff --git a/assets/js/process_definitions.js b/assets/js/process_definitions.js new file mode 100644 index 0000000..56b0289 --- /dev/null +++ b/assets/js/process_definitions.js @@ -0,0 +1,157 @@ + +document.addEventListener('DOMContentLoaded', function () { + const createProcessModal = document.getElementById('createProcessModal'); + const modalTitle = createProcessModal.querySelector('.modal-title'); + const form = createProcessModal.querySelector('#createProcessForm'); + const processIdInput = createProcessModal.querySelector('#processId'); + const processNameInput = createProcessModal.querySelector('#processName'); + const definitionJsonTextarea = createProcessModal.querySelector('#definitionJson'); + const eligibilityRulesJsonTextarea = createProcessModal.querySelector('#eligibilityRulesJson'); + + let definition = {}; + + function render() { + const statusesList = document.getElementById('statuses-list'); + const transitionsList = document.getElementById('transitions-list'); + const initialStatusSelect = document.getElementById('initialStatus'); + const fromStatusSelect = document.getElementById('fromStatusSelect'); + const toStatusSelect = document.getElementById('toStatusSelect'); + + statusesList.innerHTML = ''; + transitionsList.innerHTML = ''; + initialStatusSelect.innerHTML = ''; + fromStatusSelect.innerHTML = ''; + toStatusSelect.innerHTML = ''; + + if (definition.nodes) { + for (const nodeId in definition.nodes) { + const node = definition.nodes[nodeId]; + const statusItem = document.createElement('div'); + statusItem.textContent = node.name; + statusesList.appendChild(statusItem); + + const option = document.createElement('option'); + option.value = node.id; + option.textContent = node.name; + initialStatusSelect.appendChild(option.cloneNode(true)); + fromStatusSelect.appendChild(option.cloneNode(true)); + toStatusSelect.appendChild(option.cloneNode(true)); + } + } + + if (definition.start_node_id) { + initialStatusSelect.value = definition.start_node_id; + } + + if (definition.transitions) { + definition.transitions.forEach(transition => { + const transitionItem = document.createElement('div'); + const fromNode = definition.nodes[transition.from] ? definition.nodes[transition.from].name : 'N/A'; + const toNode = definition.nodes[transition.to] ? definition.nodes[transition.to].name : 'N/A'; + let actions = ''; + if(transition.actions) { + actions = ' - Actions: ' + JSON.stringify(transition.actions); + } + transitionItem.textContent = `${transition.name}: ${fromNode} => ${toNode}${actions}`; + transitionsList.appendChild(transitionItem); + }); + } + + if (definition.eligibility_rules) { + eligibilityRulesJsonTextarea.value = JSON.stringify(definition.eligibility_rules, null, 2); + } + } + + document.getElementById('addStatusBtn').addEventListener('click', function () { + const newStatusInput = document.getElementById('newStatusInput'); + const newStatusName = newStatusInput.value.trim(); + if (newStatusName) { + const newNodeId = (Object.keys(definition.nodes || {}).length + 1).toString(); + if (!definition.nodes) { + definition.nodes = {}; + } + definition.nodes[newNodeId] = { id: newNodeId, name: newStatusName }; + newStatusInput.value = ''; + render(); + } + }); + + document.getElementById('addTransitionBtn').addEventListener('click', function () { + const fromStatus = document.getElementById('fromStatusSelect').value; + const toStatus = document.getElementById('toStatusSelect').value; + const transitionActionJson = document.getElementById('transitionActionJson').value; + + if (fromStatus && toStatus) { + if (!definition.transitions) { + definition.transitions = []; + } + const newTransition = { + name: `Transition ${definition.transitions.length + 1}`, + from: fromStatus, + to: toStatus, + }; + + if(transitionActionJson) { + try { + newTransition.actions = JSON.parse(transitionActionJson); + } catch(e) { + alert('Invalid JSON in transition actions'); + return; + } + } + + definition.transitions.push(newTransition); + document.getElementById('transitionActionJson').value = ''; + render(); + } + }); + + form.addEventListener('submit', function (event) { + definition.start_node_id = document.getElementById('initialStatus').value; + + try { + const eligibilityRules = eligibilityRulesJsonTextarea.value; + if(eligibilityRules) { + definition.eligibility_rules = JSON.parse(eligibilityRules); + } else { + delete definition.eligibility_rules; + } + } catch(e) { + alert('Invalid JSON in eligibility rules'); + event.preventDefault(); + return; + } + + definitionJsonTextarea.value = JSON.stringify(definition, null, 2); + }); + + createProcessModal.addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const isEdit = button.classList.contains('edit-process-btn'); + + if (isEdit) { + const processId = button.dataset.processId; + const processName = button.dataset.processName; + const processDefinition = button.dataset.processDefinition; + + modalTitle.textContent = 'Edit Process'; + processIdInput.value = processId; + processNameInput.value = processName; + + try { + definition = JSON.parse(processDefinition || '{}'); + } catch(e) { + definition = {}; + } + + } else { + modalTitle.textContent = 'Create Process'; + processIdInput.value = ''; + processNameInput.value = ''; + definition = {}; + } + + eligibilityRulesJsonTextarea.value = ''; + render(); + }); +}); diff --git a/db/migrations/021_rename_camelcase_columns.php b/db/migrations/021_rename_camelcase_columns.php new file mode 100644 index 0000000..ffa7035 --- /dev/null +++ b/db/migrations/021_rename_camelcase_columns.php @@ -0,0 +1,26 @@ +exec("ALTER TABLE `process_instances` CHANGE `lastActivityAt` `last_activity_at` TIMESTAMP NULL;"); + + echo "Columns in 'process_instances' table renamed.\n"; + + // Process events table + $pdo->exec("ALTER TABLE `process_events` CHANGE `processInstanceId` `process_instance_id` INT(11) UNSIGNED NOT NULL;"); + $pdo->exec("ALTER TABLE `process_events` CHANGE `createdBy` `created_by` INT(11) UNSIGNED NOT NULL;"); + $pdo->exec("ALTER TABLE `process_events` CHANGE `createdAt` `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP;"); + + echo "Columns in 'process_events' table renamed.\n"; + + echo "Migration 021 completed successfully.\n"; + +} catch (PDOException $e) { + die("Migration 021 failed: " . $e->getMessage()); +} + diff --git a/db/migrations/022_add_is_active_to_process_definitions.php b/db/migrations/022_add_is_active_to_process_definitions.php new file mode 100644 index 0000000..4265d1e --- /dev/null +++ b/db/migrations/022_add_is_active_to_process_definitions.php @@ -0,0 +1,12 @@ +exec($sql); + echo "Migration successful: is_active column added to process_definitions table.\n"; +} catch (PDOException $e) { + die("Migration failed: " . $e->getMessage() . "\n"); +} + diff --git a/db_setup.php b/db_setup.php index 2a4fe95..1823e68 100644 --- a/db_setup.php +++ b/db_setup.php @@ -9,16 +9,16 @@ try { // 1. People table (unified table for users and contacts) $pdo->exec("CREATE TABLE IF NOT EXISTS `people` ( `id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY, - `firstName` VARCHAR(255) NOT NULL, - `lastName` VARCHAR(255) NOT NULL, + `first_name` VARCHAR(255) NOT NULL, + `last_name` VARCHAR(255) NOT NULL, `email` VARCHAR(255) NOT NULL UNIQUE, `password` VARCHAR(255) NULL, - `companyName` VARCHAR(255) DEFAULT NULL, + `company_name` VARCHAR(255) DEFAULT NULL, `phone` VARCHAR(50) DEFAULT NULL, `role` ENUM('admin', 'team_member', 'member', 'guest') NOT NULL DEFAULT 'guest', `is_user` BOOLEAN NOT NULL DEFAULT FALSE, `active` BOOLEAN NOT NULL DEFAULT TRUE, - `createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); echo "People table created or already exists.\n"; @@ -28,7 +28,7 @@ try { if ($stmt->fetchColumn() === false) { $password = password_hash('password', PASSWORD_DEFAULT); $insert_stmt = $pdo->prepare( - "INSERT INTO people (email, password, role, firstName, lastName, is_user, active) VALUES (?, ?, 'admin', 'Admin', 'User', TRUE, TRUE)" + "INSERT INTO people (email, password, role, first_name, last_name, is_user, active) VALUES (?, ?, 'admin', 'Admin', 'User', TRUE, TRUE)" ); $insert_stmt->execute(['admin@example.com', $password]); echo "Default admin user created. Email: admin@example.com, Password: password\n"; @@ -44,7 +44,7 @@ try { `is_active` BOOLEAN NOT NULL DEFAULT TRUE, `start_node_id` VARCHAR(255), `definition_json` TEXT, - `createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); echo "Process definitions table created or already exists.\n"; @@ -74,7 +74,7 @@ try { `current_reason` TEXT, `suggested_next_step` TEXT, `data_json` TEXT, - `lastActivityAt` TIMESTAMP NULL, + `last_activity_at` TIMESTAMP NULL, FOREIGN KEY (person_id) REFERENCES people(id) ON DELETE CASCADE, FOREIGN KEY (process_definition_id) REFERENCES process_definitions(id) ON DELETE CASCADE, UNIQUE KEY `person_process` (`person_id`, `process_definition_id`) @@ -84,15 +84,15 @@ try { // 4. Process Events table (updated FK) $pdo->exec("CREATE TABLE IF NOT EXISTS `process_events` ( `id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY, - `processInstanceId` INT(11) UNSIGNED NOT NULL, + `process_instance_id` INT(11) UNSIGNED NOT NULL, `event_type` VARCHAR(50) NOT NULL, `message` TEXT, `node_id` VARCHAR(255), `payload_json` TEXT, - `createdBy` INT(11) UNSIGNED NOT NULL, - `createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (processInstanceId) REFERENCES process_instances(id) ON DELETE CASCADE, - FOREIGN KEY (createdBy) REFERENCES people(id) ON DELETE CASCADE + `created_by` INT(11) UNSIGNED NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (process_instance_id) REFERENCES process_instances(id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES people(id) ON DELETE CASCADE )"); echo "Process events table created or already exists.\n"; @@ -106,8 +106,8 @@ try { if ($checkFk) { $pdo->exec("ALTER TABLE `process_instances` DROP FOREIGN KEY `{$checkFk['CONSTRAINT_NAME']}`;"); } - $pdo->exec("ALTER TABLE `process_instances` CHANGE `processId` `processDefinitionId` INT(11) UNSIGNED NOT NULL;"); - $pdo->exec("ALTER TABLE `process_instances` ADD FOREIGN KEY (`processDefinitionId`) REFERENCES `process_definitions`(`id`) ON DELETE CASCADE;"); + $pdo->exec("ALTER TABLE `process_instances` CHANGE `processId` `process_definition_id` INT(11) UNSIGNED NOT NULL;"); + $pdo->exec("ALTER TABLE `process_instances` ADD FOREIGN KEY (`process_definition_id`) REFERENCES `process_definitions`(`id`) ON DELETE CASCADE;"); echo "Migrated process_instances: processId -> processDefinitionId.\n"; } @@ -118,7 +118,7 @@ try { if ($checkFk) { $pdo->exec("ALTER TABLE `process_instances` DROP FOREIGN KEY `{$checkFk['CONSTRAINT_NAME']}`;"); } - $pdo->exec("ALTER TABLE `process_instances` CHANGE `contactId` `personId` INT(11) UNSIGNED NOT NULL;"); + $pdo->exec("ALTER TABLE `process_instances` CHANGE `contactId` `person_id` INT(11) UNSIGNED NOT NULL;"); echo "Migrated process_instances: contactId -> personId.\n"; } diff --git a/index.php b/index.php index 5eb3fe5..0587d7f 100644 --- a/index.php +++ b/index.php @@ -1,35 +1,25 @@ query(" - SELECT p.*, bg.name as bni_group_name - FROM people p - LEFT JOIN bni_groups bg ON p.bni_group_id = bg.id - ORDER BY p.lastName, p.firstName -"); -$people = $stmt->fetchAll(PDO::FETCH_ASSOC); +$workflowEngine = new WorkflowEngine(); +$matrix = $workflowEngine->getDashboardMatrix(); + +$people = $matrix['people']; +$instances = $matrix['instances']; +$all_functions = $matrix['all_functions']; +$person_functions_map = $matrix['person_functions_map']; +$bni_groups = $matrix['bni_groups']; + +// Filter out specific process definitions +$processes = array_filter($matrix['definitions'], function($process) { + return !in_array($process['name'], ['Obsluga goscia', 'Przygotowanie spotkania grupy']); +}); -// Fetch process definitions -$stmt = $pdo->prepare("SELECT * FROM process_definitions WHERE name NOT IN (?, ?)" - . " ORDER BY name"); -$stmt->execute(['Obsluga goscia', 'Przygotowanie spotkania grupy']); -$processes = $stmt->fetchAll(PDO::FETCH_ASSOC); -// Fetch all instances -$stmt = $pdo->query("SELECT * FROM process_instances"); -$all_instances = $stmt->fetchAll(PDO::FETCH_ASSOC); -// Organize instances by person and process -$instances = []; -foreach ($all_instances as $instance) { - $instances[$instance['person_id']][$instance['process_definition_id']] = $instance; -} $status_colors = [ @@ -43,23 +33,7 @@ $status_colors = [ 'inactive' => '#808080', ]; -$pdo = db(); -$stmt_functions = $pdo->query("SELECT * FROM functions ORDER BY display_order"); -$all_functions = $stmt_functions->fetchAll(PDO::FETCH_ASSOC); -$functions_by_id = []; -foreach ($all_functions as $function) { - $functions_by_id[$function['id']] = $function['name']; -} - -$stmt_person_functions = $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 = $pdo->query("SELECT * FROM bni_groups ORDER BY name"); -$bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC); ?> @@ -140,11 +114,14 @@ $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
prepare("SELECT id FROM process_definitions WHERE name = ?"); - $stmt_meeting_process->execute(['Przygotowanie spotkania grupy']); - $meeting_process = $stmt_meeting_process->fetch(PDO::FETCH_ASSOC); - $meeting_process_id = $meeting_process ? $meeting_process['id'] : 'null'; + // Find the meeting process ID from the already-fetched definitions + $meeting_process_id = 'null'; + foreach ($matrix['definitions'] as $definition) { + if ($definition['name'] === 'Przygotowanie spotkania grupy') { + $meeting_process_id = $definition['id']; + break; + } + } ?> @@ -181,9 +158,9 @@ $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC); - - - + + @@ -301,11 +270,11 @@ $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
- +
- +
@@ -348,7 +317,7 @@ $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
- +
@@ -429,11 +398,11 @@ $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
- +
- +
@@ -477,7 +446,7 @@ $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
- +
@@ -641,7 +610,7 @@ document.addEventListener('DOMContentLoaded', function () { const modalBody = instanceModal.querySelector('.modal-body'); const modalTitle = instanceModal.querySelector('.modal-title'); - fetch(`_get_instance_details.php?personId=${personId}&processId=${processId}`) + fetch(`_get_instance_details.php?person_id=${personId}&process_id=${processId}`) .then(response => response.text()) .then(html => { modalBody.innerHTML = html; @@ -902,12 +871,12 @@ document.addEventListener('DOMContentLoaded', function () { tableHtml += '
'; instances.forEach(instance => { - const person = peopleData.find(p => p.id == instance.personId); + const person = peopleData.find(p => p.id == instance.person_id); if (!person) return; tableHtml += ``; diff --git a/lib/ErrorHandler.php b/lib/ErrorHandler.php new file mode 100644 index 0000000..cf58855 --- /dev/null +++ b/lib/ErrorHandler.php @@ -0,0 +1,49 @@ +getHttpCode()); + $response = [ + 'error' => [ + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'details' => $e->getDetails(), + ], + 'correlation_id' => $correlation_id, + ]; + } else { + http_response_code(500); + error_log("Correlation ID: $correlation_id, Uncaught Exception: " . $e->getMessage() . "\n" . $e->getTraceAsString()); + $response = [ + 'error' => [ + 'code' => 500, + 'message' => 'Internal Server Error', + ], + 'correlation_id' => $correlation_id, + ]; + } + + header('Content-Type: application/json'); + echo json_encode($response); + exit; +} + +function register_error_handler() { + $GLOBALS['correlation_id'] = generate_correlation_id(); + set_exception_handler(function(Throwable $e) { + handle_exception($e, $GLOBALS['correlation_id']); + }); + // You can also set a general error handler for non-exception errors + set_error_handler(function($severity, $message, $file, $line) { + if (!(error_reporting() & $severity)) { + return; + } + error_log("Correlation ID: {$GLOBALS['correlation_id']}, Error: [$severity] $message in $file on line $line"); + // Don't output anything to the user for non-fatal errors, just log them + }); +} diff --git a/lib/WorkflowExceptions.php b/lib/WorkflowExceptions.php new file mode 100644 index 0000000..1d3a5af --- /dev/null +++ b/lib/WorkflowExceptions.php @@ -0,0 +1,44 @@ +httpCode = $httpCode; + $this->details = $details; + } + + public function getHttpCode() { + return $this->httpCode; + } + + public function getDetails() { + return $this->details; + } +} + +class WorkflowNotFoundException extends WorkflowException { + public function __construct($message = "Not Found", $details = [], Throwable $previous = null) { + parent::__construct($message, 404, 404, $details, $previous); + } +} + +class WorkflowNotAllowedException extends WorkflowException { + public function __construct($message = "Bad Request", $details = [], Throwable $previous = null) { + parent::__construct($message, 400, 400, $details, $previous); + } +} + +class WorkflowRuleFailedException extends WorkflowException { + public function __construct($message = "Unprocessable Entity", $details = [], Throwable $previous = null) { + parent::__construct($message, 422, 422, $details, $previous); + } +} + +class WorkflowConflictException extends WorkflowException { + public function __construct($message = "Conflict", $details = [], Throwable $previous = null) { + parent::__construct($message, 409, 409, $details, $previous); + } +} diff --git a/login.php b/login.php index 92f1c5f..bfe9000 100644 --- a/login.php +++ b/login.php @@ -21,7 +21,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $_SESSION['user_id'] = $person['id']; $_SESSION['user_email'] = $person['email']; $_SESSION['user_role'] = $person['role']; - $_SESSION['user_name'] = $person['firstName'] . ' ' . $person['lastName']; + $_SESSION['user_name'] = $person['first_name'] . ' ' . $person['last_name']; header('Location: index.php'); exit; } else { diff --git a/process_definitions.php b/process_definitions.php index 69d7f07..52191ad 100644 --- a/process_definitions.php +++ b/process_definitions.php @@ -101,11 +101,22 @@ $processes = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ +
+ +
+
+ +
Eligibility Rules (JSON)
+
+ +
+
@@ -119,97 +130,4 @@ $processes = $stmt->fetchAll(PDO::FETCH_ASSOC); - +
-
+
- + @@ -201,7 +178,7 @@ $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
@@ -227,51 +204,43 @@ $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC); 0 && $completedTasks === $totalTasks) { - $status = 'completed'; - } elseif ($completedTasks > 0) { - $status = 'in_progress'; - } else { - $status = 'inactive'; // Initialized but no progress - } - } - $color = $status_colors[$status] ?? $status_colors['inactive']; - ?> -
- - - - - - + data-bs-target="" + data-person-id="" + data-process-id="" + title=""> + + +
-
${htmlspecialchars(person.firstName + ' ' + person.lastName)}
+
${htmlspecialchars(person.first_name + ' ' + person.last_name)}