Mechanizm obsługi procesów

This commit is contained in:
Flatlogic Bot 2026-01-10 10:17:15 +00:00
parent 82b98582c4
commit a141f60df0
5 changed files with 231 additions and 212 deletions

View File

@ -1,12 +1,6 @@
<?php <?php
require_once __DIR__ . '/db/config.php'; require_once __DIR__ . '/db/config.php';
/**
* Class WorkflowEngine
*
* Centralny serwis do zarządzania logiką procesów.
* Interfejs użytkownika nie powinien bezpośrednio zmieniać statusów ani instancji; wszystkie operacje muszą przechodzić przez ten silnik.
*/
class WorkflowEngine { class WorkflowEngine {
private $pdo; private $pdo;
@ -15,11 +9,6 @@ class WorkflowEngine {
$this->pdo = db(); $this->pdo = db();
} }
/**
* Pobiera wszystkie dane niezbędne dla głównej macierzy pulpitu procesów.
*
* @return array Tablica zawierająca 'people', 'definitions' i zmapowaną tablicę 'instances'.
*/
public function getDashboardMatrix(): array { public function getDashboardMatrix(): array {
// Get all people (potential assignees) // 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 id, firstName, lastName, companyName, role, email, phone FROM people ORDER BY lastName, firstName");
@ -48,216 +37,179 @@ class WorkflowEngine {
]; ];
} }
/**
* Rozpoczyna nową instancję procesu dla danej osoby.
*
* @param string $processCode Unikalny kod definicji procesu.
* @param int $personId ID osoby.
* @param int $userId ID użytkownika inicjującego akcję.
* @return int|null ID nowo utworzonej instancji lub null w przypadku niepowodzenia.
*/
public function startProcess(string $processCode, int $personId, int $userId): ?int { public function startProcess(string $processCode, int $personId, int $userId): ?int {
// 1. Znajdź aktywną definicję procesu po kodzie. error_log("startProcess: processCode=$processCode, personId=$personId, userId=$userId");
// 2. Pobierz start_node_id z definicji. $this->pdo->beginTransaction();
// 3. Utwórz nową instancję procesu ze statusem 'in_progress' i current_node_id. try {
// 4. Utwórz zdarzenie systemowe dla rozpoczęcia procesu. // 1. Find active process definition by code.
// TODO: Implementacja logiki. $stmt_def = $this->pdo->prepare("SELECT * FROM process_definitions WHERE code = ? AND active = 1");
return null; $stmt_def->execute([$processCode]);
$definition = $stmt_def->fetch(PDO::FETCH_ASSOC);
error_log("startProcess: definition=" . print_r($definition, true));
if (!$definition) {
throw new Exception("Active process definition with code '$processCode' not found.");
}
$definition_json = json_decode($definition['definition_json'], true);
if (!$definition_json || !isset($definition_json['start_node_id'])) {
throw new Exception("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 (personId, processDefinitionId, current_node_id, current_status, lastActivityAt) VALUES (?, ?, ?, 'in_progress', NOW())"
);
$stmt_insert->execute([$personId, $definition['id'], $startNodeId]);
error_log("startProcess: affected rows=" . $stmt_insert->rowCount());
$instanceId = $this->pdo->lastInsertId();
error_log("startProcess: instanceId=$instanceId");
// 3. Create a system event for process start.
$this->addEvent($instanceId, 'system', 'Process started.', $startNodeId, [], $userId);
$this->pdo->commit();
return (int)$instanceId;
} catch (Exception $e) {
$this->pdo->rollBack();
error_log("Error in startProcess: " . $e->getMessage());
return null;
}
}
public function getProcessState(int $instanceId): ?array {
$stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE id = ?");
$stmt->execute([$instanceId]);
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$instance) {
return null;
}
$stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?");
$stmt_def->execute([$instance['processDefinitionId']]);
$definition_json = $stmt_def->fetchColumn();
$definition = json_decode($definition_json, true);
$currentNodeId = $instance['current_node_id'];
$nodeInfo = $definition['nodes'][$currentNodeId] ?? null;
return [
'instance' => $instance,
'definition' => $definition,
'currentNode' => $nodeInfo,
];
}
public function applyTransition(int $instanceId, string $transitionId, array $inputPayload, int $userId): bool {
$this->pdo->beginTransaction();
try {
$state = $this->getProcessState($instanceId);
if (!$state) {
throw new Exception("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) {
$transition = $t;
break;
}
}
if (!$transition) {
throw new Exception("Transition not found or not allowed from the current node.");
}
// TODO: Add rule validation here
$newNodeId = $transition['to'];
$newNodeInfo = $definition['nodes'][$newNodeId] ?? null;
// Update instance
$stmt_update = $this->pdo->prepare(
"UPDATE process_instances SET current_node_id = ?, current_status = ?, current_reason = ?, suggested_next_step = ?, lastActivityAt = NOW() WHERE id = ?"
);
$stmt_update->execute([
$newNodeId,
$newNodeInfo['ui_hints']['status'] ?? 'in_progress',
$newNodeInfo['ui_hints']['reason'] ?? '',
$newNodeInfo['ui_hints']['next_step'] ?? '',
$instanceId
]);
// Add event
$message = $inputPayload['message'] ?? $transition['name'];
$this->addEvent($instanceId, 'transition_applied', $message, $newNodeId, $inputPayload, $userId);
$this->pdo->commit();
return true;
} catch (Exception $e) {
$this->pdo->rollBack();
error_log("Error in applyTransition: " . $e->getMessage());
return false;
}
}
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, eventType, message, node_id, payload_json, createdById) VALUES (?, ?, ?, ?, ?, ?)"
);
$stmt->execute([$instanceId, $eventType, $message, $nodeId, json_encode($payload), $userId]);
} }
/**
* Pobiera pojedynczą instancję procesu, tworząc , jeśli nie istnieje.
*
* @param int $personId
* @param int $processDefinitionId
* @param int $userId Użytkownik inicjujący utworzenie, jeśli to nastąpi.
* @return array|null Tablica asocjacyjna z danymi instancji.
*/
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 personId = ? AND processDefinitionId = ?"); $stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE personId = ? AND processDefinitionId = ?");
$stmt->execute([$personId, $processDefinitionId]); $stmt->execute([$personId, $processDefinitionId]);
$instance = $stmt->fetch(PDO::FETCH_ASSOC); $instance = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$instance) { if (!$instance) {
// Fetch the process definition to get the initial status $stmt_def = $this->pdo->prepare("SELECT code FROM process_definitions WHERE id = ?");
$stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?");
$stmt_def->execute([$processDefinitionId]); $stmt_def->execute([$processDefinitionId]);
$definition_raw = $stmt_def->fetchColumn(); $processCode = $stmt_def->fetchColumn();
$definition = $definition_raw ? json_decode($definition_raw, true) : null;
$initial_status = $definition['initial_status'] ?? 'none';
$stmt_insert = $this->pdo->prepare("INSERT INTO process_instances (personId, processDefinitionId, current_status) VALUES (?, ?, ?)"); if($processCode) {
$stmt_insert->execute([$personId, $processDefinitionId, $initial_status]); $instanceId = $this->startProcess($processCode, $personId, $userId);
$instanceId = $this->pdo->lastInsertId(); if($instanceId) {
$stmt->execute([$personId, $processDefinitionId]);
// Utwórz zdarzenie systemowe dla utworzenia instancji $instance = $stmt->fetch(PDO::FETCH_ASSOC);
$this->addEvent($instanceId, 'system', 'Instancja utworzona.', null, [], $userId); }
}
// Pobierz ponownie nowo utworzoną instancję
$stmt->execute([$personId, $processDefinitionId]);
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
} }
return $instance; return $instance !== false ? $instance : null;
} }
/**
* Dodaje nowe zdarzenie do historii instancji.
* To jest wewnętrzna metoda pomocnicza.
*/
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, createdById) VALUES (?, ?, ?, ?, ?, ?)"
);
$stmt->execute([$instanceId, $eventType, $message, $nodeId, json_encode($payload), $userId]);
}
/**
* Pobiera historię zdarzeń dla danej instancji.
*
* @param int $instanceId
* @return array
*/
public function getEvents(int $instanceId): array { 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.createdById = p.id WHERE pe.processInstanceId = ? ORDER BY pe.createdAt DESC"); $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.createdById = p.id WHERE pe.processInstanceId = ? ORDER BY pe.createdAt DESC");
$stmt_events->execute([$instanceId]); $stmt_events->execute([$instanceId]);
return $stmt_events->fetchAll(PDO::FETCH_ASSOC); return $stmt_events->fetchAll(PDO::FETCH_ASSOC);
} }
/**
* Pobiera listę dostępnych przejść z bieżącego węzła instancji.
*
* @param int $instanceId
* @return array Lista dostępnych przejść.
*/
public function getAvailableTransitions(int $instanceId): array { public function getAvailableTransitions(int $instanceId): array {
$stmt = $this->pdo->prepare( $state = $this->getProcessState($instanceId);
'SELECT pi.current_status, pd.definition_json ' if (!$state) {
. 'FROM process_instances pi '
. 'JOIN process_definitions pd ON pi.processDefinitionId = pd.id '
. 'WHERE pi.id = ?'
);
$stmt->execute([$instanceId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$result || empty($result['definition_json'])) {
return []; return [];
} }
$definition = json_decode($result['definition_json'], true); $currentNodeId = $state['instance']['current_node_id'];
if (!$definition || !isset($definition['transitions'])) { $definition = $state['definition'];
return [];
}
$current_status = $result['current_status'];
$allowed_transitions = $definition['transitions'][$current_status] ?? [];
$transitions = []; $transitions = [];
foreach ($allowed_transitions as $target_status) { if (isset($definition['transitions'])) {
$transitions[] = [ foreach ($definition['transitions'] as $t) {
'id' => 'transition_' . str_replace(' ', '_', strtolower($target_status)), // e.g., transition_in_progress if ($t['from'] === $currentNodeId) {
'name' => 'Oznacz jako ' . $target_status, // e.g., Oznacz jako In Progress $transitions[] = $t;
'target_status' => $target_status }
]; }
} }
return $transitions; return $transitions;
} }
}
/**
* Stosuje przejście do instancji procesu. To jest główna metoda do postępu w przepływie pracy.
*
* @param int $instanceId ID instancji procesu.
* @param string $transitionId ID przejścia do zastosowania (z definition_json).
* @param array $inputPayload Dane zebrane od użytkownika dla tego kroku.
* @param int $userId ID użytkownika wykonującego akcję.
* @return bool True w przypadku sukcesu, false w przypadku porażki.
*/
public function applyTransition(int $instanceId, string $transitionId, array $inputPayload, int $userId): bool {
$this->pdo->beginTransaction();
try {
// 1. Pobierz instancję i dostępne przejścia
$stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE id = ?");
$stmt->execute([$instanceId]);
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$instance) {
// Instancja nie znaleziona
$this->pdo->rollBack();
return false;
}
$availableTransitions = $this->getAvailableTransitions($instanceId);
// 2. Sprawdź, czy przejście jest dozwolone
$selectedTransition = null;
if ($transitionId === 'note') { // Specjalny przypadek dodawania notatki
$selectedTransition = ['id' => 'note', 'name' => 'Dodano notatkę', 'target_status' => $instance['current_status']];
} else {
foreach ($availableTransitions as $trans) {
if ($trans['id'] === $transitionId) {
$selectedTransition = $trans;
break;
}
}
}
if (!$selectedTransition) {
// Nieprawidłowe lub niedozwolone przejście
$this->pdo->rollBack();
return false;
}
// 3. Utwórz zdarzenie
$eventType = ($transitionId === 'note') ? 'note' : 'transition_applied';
$message = $inputPayload['message'] ?? $selectedTransition['name'];
$this->addEvent($instanceId, $eventType, $message, null, $inputPayload, $userId);
// 4. Zaktualizuj instancję
$stmt_update = $this->pdo->prepare(
"UPDATE process_instances SET current_status = ?, lastActivityAt = CURRENT_TIMESTAMP WHERE id = ?"
);
$stmt_update->execute([$selectedTransition['target_status'], $instanceId]);
$this->pdo->commit();
return true;
} catch (Exception $e) {
$this->pdo->rollBack();
error_log("Błąd w applyTransition: " . $e->getMessage());
return false;
}
}
/**
* Masowa operacja stosowania tego samego przejścia do wielu osób dla danego procesu.
*
* @param string $processCode
* @param array $personIds
* @param string $transitionId
* @param array $inputPayload
* @param int $userId
* @return array Podsumowanie wyników (np. ['success' => count, 'failed' => count]).
*/
public function bulkApplyTransition(string $processCode, array $personIds, string $transitionId, array $inputPayload, int $userId): array {
// 1. Upewnij się, że instancje istnieją dla wszystkich osób (użyj ensureInstances).
// 2. Przejdź przez pętlę personIds i wywołaj applyTransition dla każdej z nich.
// TODO: Implementacja logiki.
return ['success' => 0, 'failed' => 0];
}
/**
* Zapewnia, że instancje procesów istnieją dla danego zestawu osób i kodów procesów.
* Jeśli instancja brakuje, zostanie utworzona.
*
* @param array $personIds
* @param array $processCodes
* @param int $userId
* @return array Podsumowanie utworzonych instancji.
*/
public function ensureInstances(array $personIds, array $processCodes, int $userId): array {
// TODO: Implementacja logiki do tworzenia brakujących instancji.
return ['created' => 0];
}
}

View File

@ -1,4 +1,5 @@
<?php <?php
require_once 'WorkflowEngine.php'; require_once 'WorkflowEngine.php';
session_start(); session_start();
@ -10,6 +11,8 @@ if (!isset($_SESSION['user_id'])) {
$personId = $_GET['personId'] ?? null; $personId = $_GET['personId'] ?? null;
$processDefinitionId = $_GET['processId'] ?? null; // Pulpit wysyła processId, który jest ID definicji $processDefinitionId = $_GET['processId'] ?? null; // Pulpit wysyła processId, który jest ID definicji
if (!$personId || !$processDefinitionId) { if (!$personId || !$processDefinitionId) {
http_response_code(400); http_response_code(400);
die('Brakujące parametry'); die('Brakujące parametry');
@ -20,9 +23,10 @@ $engine = new WorkflowEngine();
// 1. Pobierz lub utwórz instancję // 1. Pobierz lub utwórz instancję
$instance = $engine->getOrCreateInstanceByDefId($personId, $processDefinitionId, $userId); $instance = $engine->getOrCreateInstanceByDefId($personId, $processDefinitionId, $userId);
if (!$instance) { if (!$instance) {
http_response_code(500); http_response_code(500);
die('Nie można pobrać lub utworzyć instancji procesu.'); die("Nie można pobrać lub utworzyć instancji procesu. personId: $personId, processDefinitionId: $processDefinitionId, userId: $userId");
} }
$instanceId = $instance['id']; $instanceId = $instance['id'];

5
cookie.txt Normal file
View File

@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
localhost FALSE / FALSE 0 PHPSESSID u19ekrhqoemk4c5avca3umanfb

View File

@ -39,6 +39,7 @@ $processes = $stmt->fetchAll(PDO::FETCH_ASSOC);
<td><?= htmlspecialchars($process['name']) ?></td> <td><?= htmlspecialchars($process['name']) ?></td>
<td class="text-center"> <td class="text-center">
<button class="btn btn-sm btn-secondary edit-process-btn" <button class="btn btn-sm btn-secondary edit-process-btn"
data-bs-toggle="modal" data-bs-target="#createProcessModal"
data-process-id="<?= $process['id'] ?>" data-process-id="<?= $process['id'] ?>"
data-process-name="<?= htmlspecialchars($process['name']) ?>" data-process-name="<?= htmlspecialchars($process['name']) ?>"
data-process-definition='<?= htmlspecialchars($process['definition_json'] ?? '') ?>'> data-process-definition='<?= htmlspecialchars($process['definition_json'] ?? '') ?>'>
@ -127,22 +128,72 @@ document.addEventListener('DOMContentLoaded', function () {
const processNameInput = createProcessModal.querySelector('#processName'); const processNameInput = createProcessModal.querySelector('#processName');
const definitionJsonTextarea = createProcessModal.querySelector('#definitionJson'); const definitionJsonTextarea = createProcessModal.querySelector('#definitionJson');
// Handle create button click function renderProcessDefinition(definition) {
const createButton = document.querySelector('button[data-bs-target="#createProcessModal"]'); const statusesList = document.getElementById('statuses-list');
createButton.addEventListener('click', function() { const transitionsList = document.getElementById('transitions-list');
modalTitle.textContent = 'Create Process'; const initialStatusSelect = document.getElementById('initialStatus');
form.action = '_save_process_definition.php'; const fromStatusSelect = document.getElementById('fromStatusSelect');
processIdInput.value = ''; const toStatusSelect = document.getElementById('toStatusSelect');
processNameInput.value = '';
definitionJsonTextarea.value = '';
});
// Handle edit button click statusesList.innerHTML = '';
document.querySelectorAll('.edit-process-btn').forEach(button => { transitionsList.innerHTML = '';
button.addEventListener('click', function() { initialStatusSelect.innerHTML = '';
const processId = this.dataset.processId; fromStatusSelect.innerHTML = '';
const processName = this.dataset.processName; toStatusSelect.innerHTML = '';
const processDefinition = this.dataset.processDefinition;
if (!definition) {
return;
}
try {
const def = JSON.parse(definition);
// Populate statuses
if (def.nodes) {
for (const nodeId in def.nodes) {
const node = def.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));
}
}
// Set initial status
if (def.start_node_id) {
initialStatusSelect.value = def.start_node_id;
}
// Populate transitions
if (def.transitions) {
def.transitions.forEach(transition => {
const transitionItem = document.createElement('div');
const fromNode = def.nodes[transition.from] ? def.nodes[transition.from].name : 'N/A';
const toNode = def.nodes[transition.to] ? def.nodes[transition.to].name : 'N/A';
transitionItem.textContent = `${transition.name}: ${fromNode} => ${toNode}`;
transitionsList.appendChild(transitionItem);
});
}
} catch (e) {
console.error('Error parsing process definition:', e);
// Optionally, display an error message to the user
}
}
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'; modalTitle.textContent = 'Edit Process';
form.action = '_save_process_definition.php'; form.action = '_save_process_definition.php';
@ -150,9 +201,15 @@ document.addEventListener('DOMContentLoaded', function () {
processNameInput.value = processName; processNameInput.value = processName;
definitionJsonTextarea.value = processDefinition; definitionJsonTextarea.value = processDefinition;
const modal = new bootstrap.Modal(createProcessModal); renderProcessDefinition(processDefinition);
modal.show(); } else {
}); modalTitle.textContent = 'Create Process';
form.action = '_save_process_definition.php';
processIdInput.value = '';
processNameInput.value = '';
definitionJsonTextarea.value = '';
renderProcessDefinition(''); // Clear the form
}
}); });
}); });
</script> </script>

1
test_php.php Normal file
View File

@ -0,0 +1 @@
<?php echo "Hello from PHP"; ?>