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);
?>
- = htmlspecialchars($person['first_name'] . ' ' . $person['last_name']) ?> - = htmlspecialchars($process['name']) ?>
+ = htmlspecialchars($person['first_name']." ".$person['last_name']) ?> - = htmlspecialchars($process['name']) ?>
-
+
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
-
- $node): ?>
- ';
- } elseif ($is_completed) {
- $li_class = 'list-group-item-success';
- $status_icon = '';
- } else {
- $li_class = 'text-muted';
- $status_icon = '';
- }
-
- if ($is_available) {
- $transition = $available_transitions_map[$nodeId];
- $button = <<
- {$transition['name']}
-
-HTML;
- }
+
+
+
+
Zadania do wykonania
+
+
-
-
-
- = $status_icon ?>
-
= htmlspecialchars($node['name']) ?>
+
+ >
+
- = $button ?>
-
-
-
-
-
-
-
-
-
-
Dodaj notatkę
-
-
-
-
-
-
-
-
-
-
Historia
-
-
Brak zdarzeń.
+
+
+
-
-
- -
- = htmlspecialchars(ucfirst(str_replace('_', ' ', $event['event_type']))) ?>
- ' . htmlspecialchars($message) . '';
- }
+ 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['id']] = $t;
+ }
+
+ $visited_nodes = [];
+ foreach ($events as $event) {
+ if ($event['node_id']) {
+ $visited_nodes[$event['node_id']] = true;
+ }
+ }
+ ?>
+
+
Kroki procesu
+
+ $node):
+ $is_current = ($currentNodeId === $nodeId);
+ $is_completed = isset($visited_nodes[$nodeId]) && !$is_current;
+
+ $status_icon = '';
+ $li_class = '';
+
+ if ($is_current) {
+ $li_class = 'list-group-item-primary';
+ $status_icon = '';
+ } elseif ($is_completed) {
+ $li_class = 'list-group-item-success';
+ $status_icon = '';
+ } else {
+ $li_class = 'text-muted';
+ $status_icon = '';
+ }
?>
- Przez = htmlspecialchars($event['first_name'] . ' ' . $event['last_name']) ?> dnia = date('d.m.Y, H:i', strtotime($event['created_at'])) ?>
-
-
-
+
-
+
+ = $status_icon ?>
+ = htmlspecialchars($node['name']) ?>
+
+
+
+
+
+
Available Actions
+
+
No actions available.
+
+
+
+
+
+
+
-
+
+
+
Dodaj notatkę
+
+
+
+
+
+
+
+
+
Historia
+
+
Brak zdarzeń.
+
+
+
+ -
+ = htmlspecialchars(ucfirst(str_replace('_', ' ', $event['event_type']))) ?>
+ ' . htmlspecialchars($message) . '';
+ }
+ ?>
+ Przez = htmlspecialchars($event['first_name'] . ' ' . $event['last_name']) ?> dnia = date('d.m.Y, H:i', strtotime($event['created_at'])) ?>
+
+
+
+
+
+
+
+ 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.
+
+
+ - = htmlspecialchars($reason) ?>
+
+
+
+
+
\ 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="= $modal_target ?>"
- data-person-id="= $person['id'] ?>"
- data-process-id="= $process['id'] ?>"
+ |
+ style="cursor: pointer;"
+ data-bs-toggle="modal"
+ data-bs-target="= $modal_target ?>"
+ data-person-id="= $person['id'] ?>"
+ data-process-id="= $process['id'] ?>"
+
+ style="cursor: not-allowed;"
+
title="= htmlspecialchars($title) ?>">
= $lastActivity ?>
|