Wersja po posprzątaniu kodu

This commit is contained in:
Flatlogic Bot 2026-01-10 19:52:03 +00:00
parent 524c7007ab
commit a703aeb1e2
26 changed files with 919 additions and 548 deletions

View File

@ -12,14 +12,29 @@ class WorkflowEngine {
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 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(); $stmt_people->execute();
$people = $stmt_people->fetchAll(PDO::FETCH_ASSOC); $people = $stmt_people->fetchAll(PDO::FETCH_ASSOC);
// Fetch all process definitions // Fetch all process definitions with their JSON
$stmt_defs = $this->pdo->prepare("SELECT id, name FROM process_definitions ORDER BY name"); $stmt_defs = $this->pdo->prepare("SELECT id, name, definition_json FROM process_definitions ORDER BY name");
$stmt_defs->execute(); $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 // Fetch instances
$stmt_instances = $this->pdo->prepare("SELECT * FROM process_instances"); $stmt_instances = $this->pdo->prepare("SELECT * FROM process_instances");
@ -28,21 +43,94 @@ class WorkflowEngine {
$instances = []; $instances = [];
foreach ($instances_data as $instance) { 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 [ return [
'people' => $people, 'people' => $people,
'definitions' => $process_definitions, 'definitions' => array_values($definitions),
'instances' => $instances, '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(); $this->pdo->beginTransaction();
try { try {
// 1. Find active process definition by code. // 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]); $stmt_def->execute([$processCode]);
$definition = $stmt_def->fetch(PDO::FETCH_ASSOC); $definition = $stmt_def->fetch(PDO::FETCH_ASSOC);
@ -53,12 +141,12 @@ class WorkflowEngine {
$definition = $stmt_def->fetch(PDO::FETCH_ASSOC); $definition = $stmt_def->fetch(PDO::FETCH_ASSOC);
if (!$definition) { 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) : []; $definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : [];
if (empty($definition_json) || $definition_json['type'] !== 'checklist') { 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 // For checklists, there's no start_node_id, so we can proceed with instance creation
@ -67,14 +155,14 @@ class WorkflowEngine {
} else { } else {
$definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : []; $definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : [];
if (empty($definition_json) || !isset($definition_json['start_node_id'])) { 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']; $startNodeId = $definition_json['start_node_id'];
} }
// 2. Create a new process instance. // 2. Create a new process instance.
$stmt_insert = $this->pdo->prepare( $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]); $stmt_insert->execute([$personId, $definition['id'], $startNodeId]);
$instanceId = $this->pdo->lastInsertId(); $instanceId = $this->pdo->lastInsertId();
@ -86,8 +174,7 @@ class WorkflowEngine {
return (int)$instanceId; return (int)$instanceId;
} catch (Exception $e) { } catch (Exception $e) {
$this->pdo->rollBack(); $this->pdo->rollBack();
error_log("Error in startProcess: " . $e->getMessage()); throw $e;
return null;
} }
} }
@ -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(); $this->pdo->beginTransaction();
try { try {
$state = $this->getProcessState($instanceId); $state = $this->getProcessState($instanceId);
if (!$state) { if (!$state) {
throw new Exception("Process instance not found."); throw new WorkflowNotFoundException("Process instance not found.");
} }
$instance = $state['instance']; $instance = $state['instance'];
$definition = $state['definition']; $definition = $state['definition'];
$currentNodeId = $instance['current_node_id']; $currentNodeId = $instance['current_node_id'];
// Find the transition from the definition
$transition = null; $transition = null;
foreach ($definition['transitions'] as $t) { foreach ($definition['transitions'] as $t) {
if ($t['from'] === $currentNodeId && $t['id'] === $transitionId) { if ($t['from'] === $currentNodeId && $t['id'] === $transitionId) {
@ -137,7 +223,7 @@ class WorkflowEngine {
} }
if (!$transition) { 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 // TODO: Add rule validation here
@ -145,37 +231,53 @@ class WorkflowEngine {
$newNodeId = $transition['to']; $newNodeId = $transition['to'];
$newNodeInfo = $definition['nodes'][$newNodeId] ?? null; $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( $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([ $stmt_update->execute([
$newNodeId, $newNodeId,
$newNodeInfo['ui_hints']['status'] ?? 'in_progress', $newStatus,
$newNodeInfo['ui_hints']['reason'] ?? '', $newReason,
$newNodeInfo['ui_hints']['next_step'] ?? '', $newNextStep,
$instanceId $instanceId
]); ]);
// Add event
$message = $inputPayload['message'] ?? $transition['name']; $message = $inputPayload['message'] ?? $transition['name'];
$this->addEvent($instanceId, 'transition_applied', $message, $newNodeId, $inputPayload, $userId); $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(); $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) { } catch (Exception $e) {
$this->pdo->rollBack(); $this->pdo->rollBack();
error_log("Error in applyTransition: " . $e->getMessage()); // Re-throw the original exception to be handled by the global error handler
return false; throw $e;
} }
} }
public function addNote(int $instanceId, string $message, int $userId): bool { public function addNote(int $instanceId, string $message, int $userId): bool {
$state = $this->getProcessState($instanceId); $state = $this->getProcessState($instanceId);
if (!$state) { if (!$state) {
// Even if the instance isn't found, we shouldn't crash. Log and return false. throw new WorkflowNotFoundException("Process instance #$instanceId not found.");
error_log("addNote failed: Process instance $instanceId not found.");
return false;
} }
$currentNodeId = $state['instance']['current_node_id']; $currentNodeId = $state['instance']['current_node_id'];
$payload = ['message' => $message]; $payload = ['message' => $message];
@ -183,33 +285,145 @@ class WorkflowEngine {
return true; 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 { private function addEvent(int $instanceId, string $eventType, string $message, ?string $nodeId, array $payload, int $userId): void {
$stmt = $this->pdo->prepare( $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]); $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 = $this->pdo->prepare("SELECT * FROM process_instances WHERE `person_id` = ? AND `process_definition_id` = ?");
$stmt->execute([$personId, $processDefinitionId]); $stmt->execute([$personId, $processDefinitionId]);
$instance = $stmt->fetch(PDO::FETCH_ASSOC); $instance = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$instance) { if (!$instance) {
// For checklists, the process code is the process definition ID $stmt_def = $this->pdo->prepare("SELECT definition_json, 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_json = $stmt_def->fetchColumn(); $definition = $stmt_def->fetch(PDO::FETCH_ASSOC);
$definition = !empty($definition_json) ? json_decode($definition_json, true) : [];
if ($definition && isset($definition['type']) && $definition['type'] === 'checklist') { if (!$definition) {
$processCode = (string) $processDefinitionId; throw new WorkflowNotFoundException("Process definition #$processDefinitionId not found.");
} else {
$stmt_def = $this->pdo->prepare("SELECT code FROM process_definitions WHERE id = ?");
$stmt_def->execute([$processDefinitionId]);
$processCode = $stmt_def->fetchColumn();
} }
$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) { if($processCode) {
$instanceId = $this->startProcess($processCode, $personId, $userId); $instanceId = $this->startProcess($processCode, $personId, $userId);
if($instanceId) { if($instanceId) {
@ -219,11 +433,11 @@ class WorkflowEngine {
} }
} }
return $instance !== false ? $instance : null; return $instance ?: null;
} }
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.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]); $stmt_events->execute([$instanceId]);
return $stmt_events->fetchAll(PDO::FETCH_ASSOC); return $stmt_events->fetchAll(PDO::FETCH_ASSOC);
} }
@ -261,4 +475,45 @@ class WorkflowEngine {
$definition = !empty($json) ? json_decode($json, true) : []; $definition = !empty($json) ? json_decode($json, true) : [];
return $definition['nodes'] ?? []; 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);
}
}
} }

View File

@ -1,31 +1,28 @@
<?php <?php
require_once 'db/config.php'; require_once 'lib/ErrorHandler.php';
require_once 'WorkflowEngine.php';
session_start(); session_start();
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
http_response_code(401); throw new WorkflowNotAllowedException('Unauthorized');
die('Unauthorized');
} }
$pdo = db(); $instanceId = $_POST['instance_id'] ?? null;
$instanceId = $_POST['instanceId'] ?? null; $message = $_POST['message'] ?? null;
$eventType = $_POST['eventType'] ?? null;
$description = $_POST['description'] ?? null;
$userId = $_SESSION['user_id']; $userId = $_SESSION['user_id'];
if (!$instanceId || !$eventType) { if (!$instanceId || !$message) {
http_response_code(400); throw new WorkflowRuleFailedException('Missing parameters: instance_id and message are required.');
die('Missing parameters');
} }
// Create the event $workflowEngine = new WorkflowEngine();
$stmt = $pdo->prepare("INSERT INTO process_events (processInstanceId, eventType, description, createdBy) VALUES (?, ?, ?, ?)"); $workflowEngine->addNote((int)$instanceId, $message, (int)$userId);
$stmt->execute([$instanceId, $eventType, $description, $userId]);
// Update the last activity time for the instance if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) {
$stmt_update = $pdo->prepare("UPDATE process_instances SET lastActivityAt = NOW() WHERE id = ?"); header('Content-Type: application/json');
$stmt_update->execute([$instanceId]); echo json_encode(['message' => 'Note added successfully.']);
} else {
// Redirect back to the dashboard header('Location: ' . $_SERVER['HTTP_REFERER']);
header("Location: process_dashboard.php"); }
exit; exit;

View File

@ -1,23 +1,18 @@
<?php <?php
session_start(); session_start();
require_once 'lib/ErrorHandler.php';
register_error_handler();
header('Content-Type: application/json'); header('Content-Type: application/json');
$response = ['success' => false, 'message' => 'Wystąpił nieoczekiwany błąd.'];
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405); throw new WorkflowNotAllowedException('Invalid request method.');
$response['message'] = 'Invalid request method.';
echo json_encode($response);
exit;
} }
require_once 'WorkflowEngine.php'; require_once 'WorkflowEngine.php';
if (!isset($_POST['instanceId']) || !isset($_POST['transitionId'])) { if (!isset($_POST['instanceId']) || !isset($_POST['transitionId'])) {
http_response_code(400); throw new WorkflowNotAllowedException('Błąd: Brak wymaganych parametrów.');
$response['message'] = 'Błąd: Brak wymaganych parametrów.';
echo json_encode($response);
exit;
} }
$instanceId = (int)$_POST['instanceId']; $instanceId = (int)$_POST['instanceId'];
@ -26,42 +21,21 @@ $userId = $_SESSION['user_id'] ?? null;
$payload = $_POST['payload'] ?? []; $payload = $_POST['payload'] ?? [];
if (!$userId) { if (!$userId) {
http_response_code(401); throw new WorkflowNotAllowedException('Błąd: Sesja wygasła.', [], 401);
$response['message'] = 'Błąd: Sesja wygasła.';
echo json_encode($response);
exit;
} }
try { $engine = new WorkflowEngine();
$engine = new WorkflowEngine();
$success = false;
if ($transitionId === 'note') { if ($transitionId === 'note') {
// Special case: Just add a note, don't change state. $message = $payload['message'] ?? '';
$message = $payload['message'] ?? ''; if (empty($message)) {
if (!empty($message)) { throw new WorkflowNotAllowedException('Treść notatki nie może być pusta.');
$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.';
}
} }
$engine->addNote($instanceId, $message, $userId);
if ($success) { $response = ['success' => true, 'message' => 'Notatka została dodana.'];
$response['success'] = true; } else {
} $result = $engine->applyTransition($instanceId, $transitionId, $payload, $userId);
$response = ['success' => true, 'message' => 'Akcja została wykonana pomyślnie.', 'data' => $result];
} catch (Exception $e) {
error_log("Error applying transition: " . $e->getMessage());
http_response_code(500);
$response['message'] = 'Wystąpił krytyczny błąd: ' . $e->getMessage();
} }
echo json_encode($response); echo json_encode($response);
exit;

View File

@ -1,53 +1,50 @@
<?php <?php
require_once 'lib/ErrorHandler.php';
require_once 'WorkflowEngine.php';
require_once 'db/config.php'; require_once 'db/config.php';
session_start(); session_start();
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
http_response_code(401); throw new WorkflowNotAllowedException('Unauthorized');
die('Unauthorized');
} }
$pdo = db();
$person_ids = json_decode($_POST['person_ids'] ?? '[]'); $person_ids = json_decode($_POST['person_ids'] ?? '[]');
$process_id = $_POST['process_id'] ?? null; $process_id = $_POST['process_id'] ?? null;
$message = $_POST['description'] ?? null; // The form sends 'description' $message = $_POST['description'] ?? null;
$userId = $_SESSION['user_id']; $userId = $_SESSION['user_id'];
if (empty($person_ids) || !$process_id || !$message) { if (empty($person_ids) || !$process_id || !$message) {
http_response_code(400); throw new WorkflowRuleFailedException('Missing parameters: person_ids, process_id, and description are required.');
die('Missing parameters');
} }
$pdo = db();
$placeholders = implode(',', array_fill(0, count($person_ids), '?')); $placeholders = implode(',', array_fill(0, count($person_ids), '?'));
$stmt = $pdo->prepare("SELECT id FROM process_instances WHERE process_definition_id = ? AND person_id IN ($placeholders)");
// Get all relevant instance IDs
$stmt = $pdo->prepare("SELECT id FROM process_instances WHERE processDefinitionId = ? AND personId IN ($placeholders)");
$params = array_merge([$process_id], $person_ids); $params = array_merge([$process_id], $person_ids);
$stmt->execute($params); $stmt->execute($params);
$instance_ids = $stmt->fetchAll(PDO::FETCH_COLUMN); $instance_ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (!empty($instance_ids)) { if (empty($instance_ids)) {
$instance_placeholders = implode(',', array_fill(0, count($instance_ids), '?')); $_SESSION['flash_message'] = "No instances found for the selected people and process.";
header('Location: ' . $_SERVER['HTTP_REFERER']);
// Update last activity exit;
$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);
} }
$_SESSION['flash_message'] = "Bulk event addition completed."; $notes = [];
header('Location: process_dashboard.php'); 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; exit;

View File

@ -1,33 +1,56 @@
<?php <?php
require_once 'db/config.php'; require_once 'WorkflowEngine.php';
require_once 'lib/ErrorHandler.php';
require_once 'lib/WorkflowExceptions.php';
session_start(); session_start();
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
http_response_code(401); throw new WorkflowNotAllowedException('Unauthorized');
die('Unauthorized');
} }
$pdo = db(); $userId = $_SESSION['user_id'];
$person_ids = json_decode($_POST['person_ids'] ?? '[]'); $personIds = $_POST['personIds'] ?? '[]';
if (is_string($personIds)) {
$personIds = json_decode($personIds, true);
}
$process_id = $_POST['process_id'] ?? null; $process_id = $_POST['process_id'] ?? null;
if (empty($person_ids) || !$process_id) { if (empty($personIds) || !$process_id) {
http_response_code(400); throw new WorkflowRuleFailedException('Missing parameters');
die('Missing parameters');
} }
$sql = "INSERT IGNORE INTO process_instances (personId, processDefinitionId, current_status) VALUES "; $engine = new WorkflowEngine();
$rows = []; $results = [
$params = []; 'success' => [],
foreach($person_ids as $person_id) { 'failed' => [],
$rows[] = "(?, ?, 'none')"; ];
$params[] = $person_id;
$params[] = $process_id;
}
$sql .= implode(', ', $rows);
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$_SESSION['flash_message'] = "Bulk initialization completed."; foreach ($personIds as $personId) {
header('Location: process_dashboard.php'); try {
exit; $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();

View File

@ -1,54 +1,52 @@
<?php <?php
require_once 'lib/ErrorHandler.php';
require_once 'WorkflowEngine.php';
require_once 'db/config.php'; require_once 'db/config.php';
session_start(); session_start();
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
http_response_code(401); throw new WorkflowNotAllowedException('Unauthorized');
die('Unauthorized');
} }
$pdo = db();
$person_ids = json_decode($_POST['person_ids'] ?? '[]'); $person_ids = json_decode($_POST['person_ids'] ?? '[]');
$process_id = $_POST['process_id'] ?? null; $process_id = $_POST['process_id'] ?? null;
$status = $_POST['status'] ?? null; $status = $_POST['status'] ?? null;
$reason = $_POST['reason'] ?? '';
$userId = $_SESSION['user_id']; $userId = $_SESSION['user_id'];
if (empty($person_ids) || !$process_id || !$status) { if (empty($person_ids) || !$process_id || !$status) {
http_response_code(400); throw new WorkflowRuleFailedException('Missing parameters: person_ids, process_id, and status are required.');
die('Missing parameters');
} }
$pdo = db();
$placeholders = implode(',', array_fill(0, count($person_ids), '?')); $placeholders = implode(',', array_fill(0, count($person_ids), '?'));
$stmt = $pdo->prepare("SELECT id FROM process_instances WHERE process_definition_id = ? AND person_id IN ($placeholders)");
// Get all relevant instance IDs
$stmt = $pdo->prepare("SELECT id FROM process_instances WHERE processDefinitionId = ? AND personId IN ($placeholders)");
$params = array_merge([$process_id], $person_ids); $params = array_merge([$process_id], $person_ids);
$stmt->execute($params); $stmt->execute($params);
$instance_ids = $stmt->fetchAll(PDO::FETCH_COLUMN); $instance_ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (!empty($instance_ids)) { if (empty($instance_ids)) {
$instance_placeholders = implode(',', array_fill(0, count($instance_ids), '?')); $_SESSION['flash_message'] = "No instances found for the selected people and process.";
header('Location: ' . $_SERVER['HTTP_REFERER']);
// Update statuses exit;
$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);
} }
$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."; $_SESSION['flash_message'] = "Bulk status update completed.";
header('Location: process_dashboard.php'); $_SESSION['bulk_results'] = $results;
header('Location: ' . $_SERVER['HTTP_REFERER']);
exit; exit;

View File

@ -3,11 +3,11 @@ require_once 'db/config.php';
session_start(); session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$firstName = $_POST['firstName']; $first_name = $_POST['first_name'];
$lastName = $_POST['lastName']; $last_name = $_POST['last_name'];
$email = $_POST['email']; $email = $_POST['email'];
$password = $_POST['password']; $password = $_POST['password'];
$companyName = $_POST['companyName'] ?? null; $company_name = $_POST['company_name'] ?? null;
$phone = $_POST['phone'] ?? null; $phone = $_POST['phone'] ?? null;
$role = $_POST['role'] ?? 'guest'; $role = $_POST['role'] ?? 'guest';
$functions = isset($_POST['functions']) ? $_POST['functions'] : []; $functions = isset($_POST['functions']) ? $_POST['functions'] : [];
@ -35,9 +35,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$pdo->beginTransaction(); $pdo->beginTransaction();
// Insert person details first // 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 = $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(); $personId = $pdo->lastInsertId();
// Handle file uploads now that we have a personId // Handle file uploads now that we have a personId

View File

@ -1,19 +1,20 @@
<?php <?php
require_once 'WorkflowEngine.php'; require_once 'WorkflowEngine.php';
require_once 'lib/ErrorHandler.php';
require_once 'lib/WorkflowExceptions.php';
session_start(); session_start();
// Security check // Security check
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
http_response_code(401); throw new WorkflowNotAllowedException('Brak autoryzacji');
die('Brak autoryzacji');
} }
$personId = $_GET['personId'] ?? null; $person_id = $_GET['person_id'] ?? null;
$processDefinitionId = $_GET['processId'] ?? null; $process_definition_id = $_GET['process_id'] ?? null;
if (!$personId || !$processDefinitionId) { if (!$person_id || !$process_definition_id) {
http_response_code(400); throw new WorkflowRuleFailedException('Brakujące parametry');
die('Brakujące parametry');
} }
$userId = $_SESSION['user_id']; $userId = $_SESSION['user_id'];
@ -21,20 +22,19 @@ $engine = new WorkflowEngine();
$pdo = db(); $pdo = db();
// 1. Get or create instance // 1. Get or create instance
$instance = $engine->getOrCreateInstanceByDefId($personId, $processDefinitionId, $userId); $instance = $engine->getOrCreateInstanceByDefId($person_id, $process_definition_id, $userId);
if (!$instance) { if (!$instance) {
http_response_code(500); throw new WorkflowNotFoundException("Nie można pobrać lub utworzyć instancji procesu.");
die("Nie można pobrać lub utworzyć instancji procesu.");
} }
$instanceId = $instance['id']; $instanceId = $instance['id'];
// 2. Fetch all related data // 2. Fetch all related data
$stmt_person = $pdo->prepare("SELECT firstName, lastName FROM people WHERE id = ?"); $stmt_person = $pdo->prepare("SELECT first_name, last_name FROM people WHERE id = ?");
$stmt_person->execute([$personId]); $stmt_person->execute([$person_id]);
$person = $stmt_person->fetch(); $person = $stmt_person->fetch();
$stmt_process = $pdo->prepare("SELECT * FROM process_definitions WHERE id = ?"); $stmt_process = $pdo->prepare("SELECT * FROM process_definitions WHERE id = ?");
$stmt_process->execute([$processDefinitionId]); $stmt_process->execute([$process_definition_id]);
$process = $stmt_process->fetch(); $process = $stmt_process->fetch();
$definition = $process && $process['definition_json'] ? json_decode($process['definition_json'], true) : null; $definition = $process && $process['definition_json'] ? json_decode($process['definition_json'], true) : null;
$isChecklist = ($definition && isset($definition['type']) && $definition['type'] === 'checklist'); $isChecklist = ($definition && isset($definition['type']) && $definition['type'] === 'checklist');
@ -45,7 +45,7 @@ $events = $engine->getEvents($instanceId);
<!-- Title for the modal, to be grabbed by JS --> <!-- Title for the modal, to be grabbed by JS -->
<div id="instance-modal-title" class="d-none"> <div id="instance-modal-title" class="d-none">
<?= htmlspecialchars($person['firstName'] . ' ' . $person['lastName']) ?> - <?= htmlspecialchars($process['name']) ?> <?= htmlspecialchars($person['first_name'] . ' ' . $person['last_name']) ?> - <?= htmlspecialchars($process['name']) ?>
</div> </div>
<?php if ($isChecklist): ?> <?php if ($isChecklist): ?>
@ -73,7 +73,7 @@ $events = $engine->getEvents($instanceId);
<?php else: ?> <?php else: ?>
<?php <?php
$currentNodeId = $instance['current_node_id']; $currentNodeId = $instance['current_node_id'];
$all_nodes = $engine->getProcessDefinitionNodes($processDefinitionId); $all_nodes = $engine->getProcessDefinitionNodes($process_definition_id);
$availableTransitions = $engine->getAvailableTransitions($instanceId); $availableTransitions = $engine->getAvailableTransitions($instanceId);
$available_target_node_ids = array_map(function($t) { return $t['to']; }, $availableTransitions); $available_target_node_ids = array_map(function($t) { return $t['to']; }, $availableTransitions);
@ -164,7 +164,7 @@ HTML;
echo '<p class="mb-1 text-muted fst-italic">' . htmlspecialchars($message) . '</p>'; echo '<p class="mb-1 text-muted fst-italic">' . htmlspecialchars($message) . '</p>';
} }
?> ?>
<small class="text-muted">Przez <?= htmlspecialchars($event['firstName'] . ' ' . $event['lastName']) ?> dnia <?= date('d.m.Y, H:i', strtotime($event['created_at'])) ?></small> <small class="text-muted">Przez <?= htmlspecialchars($event['first_name'] . ' ' . $event['last_name']) ?> dnia <?= date('d.m.Y, H:i', strtotime($event['created_at'])) ?></small>
</li> </li>
<?php endforeach; ?> <?php endforeach; ?>
</ul> </ul>

View File

@ -35,7 +35,7 @@ try {
} }
// 2. Get all instances for this process // 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]); $stmt_instances->execute([$process_id]);
$instances = $stmt_instances->fetchAll(PDO::FETCH_ASSOC); $instances = $stmt_instances->fetchAll(PDO::FETCH_ASSOC);
@ -45,29 +45,29 @@ try {
$events = []; $events = [];
if (!empty($instance_ids)) { if (!empty($instance_ids)) {
$placeholders = implode(',', array_fill(0, count($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); $stmt_events->execute($instance_ids);
$all_events = $stmt_events->fetchAll(PDO::FETCH_ASSOC); $all_events = $stmt_events->fetchAll(PDO::FETCH_ASSOC);
// Group events by instance_id // Group events by instance_id
foreach ($all_events as $event) { foreach ($all_events as $event) {
$events[$event['processInstanceId']][] = $event; $events[$event['process_instance_id']][] = $event;
} }
} }
// 4. Get People details // 4. Get People details
$people_ids = array_unique(array_column($instances, 'personId')); $people_ids = array_unique(array_column($instances, 'person_id'));
$people = []; $people = [];
if (!empty($people_ids)) { if (!empty($people_ids)) {
$valid_people_ids = array_filter($people_ids, 'is_numeric'); $valid_people_ids = array_filter($people_ids, 'is_numeric');
if (!empty($valid_people_ids)) { if (!empty($valid_people_ids)) {
$placeholders = implode(',', array_fill(0, count($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)); $stmt_people->execute(array_values($valid_people_ids));
$people_results = $stmt_people->fetchAll(PDO::FETCH_ASSOC); $people_results = $stmt_people->fetchAll(PDO::FETCH_ASSOC);
foreach ($people_results as $person) { foreach ($people_results as $person) {
$people[$person['id']] = $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']);
} }
} }
} }

View File

@ -1,5 +1,7 @@
<?php <?php
session_start(); session_start();
require_once __DIR__ . '/lib/ErrorHandler.php';
register_error_handler();
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
header('Location: login.php'); header('Location: login.php');

View File

@ -19,12 +19,12 @@ $stmt_processes = $pdo->prepare("SELECT id FROM process_definitions WHERE is_act
$stmt_processes->execute(); $stmt_processes->execute();
$processes = $stmt_processes->fetchAll(PDO::FETCH_COLUMN); $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; $count = 0;
foreach ($people as $personId) { foreach ($people as $person_id) {
foreach ($processes as $processId) { foreach ($processes as $process_id) {
$insert_stmt->execute([$personId, $processId]); $insert_stmt->execute([$person_id, $process_id]);
if ($insert_stmt->rowCount() > 0) { if ($insert_stmt->rowCount() > 0) {
$count++; $count++;
} }

View File

@ -1,11 +1,11 @@
<?php <?php
session_start(); session_start();
require_once 'WorkflowEngine.php'; require_once 'WorkflowEngine.php';
require_once 'lib/ErrorHandler.php';
require_once 'lib/WorkflowExceptions.php';
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
$_SESSION['error_message'] = "Authentication required."; throw new WorkflowNotAllowedException('Authentication required.');
header('Location: login.php');
exit();
} }
$userId = $_SESSION['user_id']; $userId = $_SESSION['user_id'];
@ -13,9 +13,7 @@ $personId = $_GET['personId'] ?? null;
$processDefinitionId = $_GET['processId'] ?? null; $processDefinitionId = $_GET['processId'] ?? null;
if (!$personId || !$processDefinitionId) { if (!$personId || !$processDefinitionId) {
$_SESSION['error_message'] = "Missing parameters for process initialization."; throw new WorkflowRuleFailedException('Missing parameters for process initialization.');
header('Location: index.php');
exit();
} }
$engine = new WorkflowEngine(); $engine = new WorkflowEngine();

View File

@ -1,49 +1,49 @@
<?php <?php
require_once 'db/config.php'; require_once 'db/config.php';
require_once 'lib/ErrorHandler.php';
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$processId = $_POST['process_id'] ?? null; $processId = $_POST['process_id'] ?? null;
$name = $_POST['name']; $name = $_POST['name'] ?? '';
$definition_json = $_POST['definition_json']; $definition_json = $_POST['definition_json'] ?? '';
if (empty($name)) { if (empty($name)) {
die('Process name is required.'); throw new WorkflowRuleFailedException('Process name is required.');
} }
// Validate JSON // Validate JSON
if (!empty($definition_json)) { if (!empty($definition_json)) {
json_decode($definition_json); json_decode($definition_json);
if (json_last_error() !== JSON_ERROR_NONE) { if (json_last_error() !== JSON_ERROR_NONE) {
die('Invalid JSON format in definition.'); throw new WorkflowRuleFailedException('Invalid JSON format in definition.');
} }
} }
try { $pdo = db();
$pdo = db();
if (empty($processId)) { if (empty($processId)) {
// Create new process // Create new process
$sql = 'INSERT INTO process_definitions (name, definition_json) VALUES (?, ?)'; $sql = 'INSERT INTO process_definitions (name, definition_json, is_active) VALUES (?, ?, 1)';
$params = [$name, $definition_json]; $params = [$name, $definition_json, 1];
$message = 'Process created successfully.'; $message = 'Process created successfully.';
} else { } else {
// Update existing process // Update existing process
$sql = 'UPDATE process_definitions SET name = ?, definition_json = ? WHERE id = ?'; $sql = 'UPDATE process_definitions SET name = ?, definition_json = ? WHERE id = ?';
$params = [$name, $definition_json, $processId]; $params = [$name, $definition_json, $processId];
$message = 'Process updated successfully.'; $message = 'Process updated successfully.';
} }
$stmt = $pdo->prepare($sql); $stmt = $pdo->prepare($sql);
$stmt->execute($params); $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; $_SESSION['success_message'] = $message;
header('Location: process_definitions.php');
} 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');
exit(); exit();
} }

View File

@ -1,13 +1,13 @@
<?php <?php
session_start(); session_start();
require_once 'db/config.php';
require_once 'lib/ErrorHandler.php';
require_once 'lib/WorkflowExceptions.php';
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
header("Location: login.php"); throw new WorkflowNotAllowedException('Authentication required.');
exit();
} }
require_once 'db/config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$event_id = $_POST['event_id'] ?? null; $event_id = $_POST['event_id'] ?? null;
$title = $_POST['title'] ?? ''; $title = $_POST['title'] ?? '';
@ -19,8 +19,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$group_ids = $_POST['group_ids'] ?? []; $group_ids = $_POST['group_ids'] ?? [];
if (empty($event_id) || empty($title) || empty($event_type_id) || !is_array($group_ids)) { if (empty($event_id) || empty($title) || empty($event_type_id) || !is_array($group_ids)) {
header("Location: calendar.php?error=empty_fields"); throw new WorkflowRuleFailedException('Empty fields');
exit();
} }
$pdo = db(); $pdo = db();
@ -77,7 +76,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} catch (Exception $e) { } catch (Exception $e) {
$pdo->rollBack(); $pdo->rollBack();
error_log("Error updating event: " . $e->getMessage()); error_log("Error updating event: " . $e->getMessage());
header("Location: calendar.php?error=db_error"); throw $e;
exit();
} }
} }

View File

@ -1,30 +1,29 @@
<?php <?php
require_once 'db/config.php'; require_once 'lib/ErrorHandler.php';
require_once 'WorkflowEngine.php';
session_start(); session_start();
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
http_response_code(401); throw new WorkflowNotAllowedException('Unauthorized');
die('Unauthorized');
} }
$pdo = db(); $instanceId = $_POST['instance_id'] ?? null;
$instanceId = $_POST['instanceId'] ?? null;
$status = $_POST['status'] ?? null; $status = $_POST['status'] ?? null;
$reason = $_POST['reason'] ?? '';
$userId = $_SESSION['user_id']; $userId = $_SESSION['user_id'];
if (!$instanceId || !$status) { if (!$instanceId || !$status) {
http_response_code(400); throw new WorkflowRuleFailedException('Missing parameters: instance_id and status are required.');
die('Missing parameters');
} }
// Update instance status and last activity time $workflowEngine = new WorkflowEngine();
$stmt = $pdo->prepare("UPDATE process_instances SET status = ?, lastActivityAt = NOW() WHERE id = ?"); $workflowEngine->applyManualStatus((int)$instanceId, $status, $reason, (int)$userId);
$stmt->execute([$status, $instanceId]);
// Create a status change event if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) {
$stmt_event = $pdo->prepare("INSERT INTO process_events (processInstanceId, eventType, description, createdBy) VALUES (?, 'status_change', ?, ?)"); header('Content-Type: application/json');
$stmt_event->execute([$instanceId, "Status changed to $status", $userId]); echo json_encode(['message' => 'Status updated successfully.']);
} else {
// Redirect back to the dashboard header('Location: index.php');
header("Location: process_dashboard.php"); }
exit; exit;

View File

@ -4,10 +4,10 @@ session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$personId = $_POST['id']; $personId = $_POST['id'];
$firstName = $_POST['firstName']; $first_name = $_POST['first_name'];
$lastName = $_POST['lastName']; $last_name = $_POST['last_name'];
$email = $_POST['email']; $email = $_POST['email'];
$companyName = $_POST['companyName']; $company_name = $_POST['company_name'];
$phone = $_POST['phone']; $phone = $_POST['phone'];
$role = $_POST['role'] ?? 'guest'; $role = $_POST['role'] ?? 'guest';
$functions = isset($_POST['functions']) ? $_POST['functions'] : []; $functions = isset($_POST['functions']) ? $_POST['functions'] : [];
@ -61,12 +61,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Prepare SQL for updating person details // Prepare SQL for updating person details
$sql_parts = [ $sql_parts = [
'firstName = ?', 'lastName = ?', 'email = ?', 'companyName = ?', 'phone = ?', 'first_name = ?', 'last_name = ?', 'email = ?', 'company_name = ?', 'phone = ?',
'role = ?', 'bni_group_id = ?', 'nip = ?', 'industry = ?', 'company_size_revenue = ?', 'role = ?', 'bni_group_id = ?', 'nip = ?', 'industry = ?', 'company_size_revenue = ?',
'business_description = ?' 'business_description = ?'
]; ];
$params = [ $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 $nip, $industry, $company_size_revenue, $business_description
]; ];

View File

@ -1,81 +1,36 @@
<?php <?php
require_once 'db/config.php'; require_once 'lib/ErrorHandler.php';
require_once 'WorkflowEngine.php';
session_start(); session_start();
header('Content-Type: application/json');
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
http_response_code(401); throw new WorkflowNotAllowedException('Unauthorized');
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
} }
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405); throw new WorkflowNotAllowedException('Method Not Allowed');
echo json_encode(['error' => 'Method Not Allowed']);
exit;
} }
$inputJSON = file_get_contents('php://input'); $inputJSON = file_get_contents('php://input');
$input = json_decode($inputJSON, true); $input = json_decode($inputJSON, true);
if (json_last_error() !== JSON_ERROR_NONE) { if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400); throw new WorkflowRuleFailedException('Invalid JSON');
echo json_encode(['error' => 'Invalid JSON']);
exit;
} }
$instanceId = $input['instance_id'] ?? null; $instanceId = $input['instance_id'] ?? null;
$taskCode = $input['task_code'] ?? null; $taskCode = $input['task_code'] ?? null;
$isChecked = $input['is_checked'] ?? null; $isChecked = $input['is_checked'] ?? null;
$userId = $_SESSION['user_id'];
if (!$instanceId || !$taskCode || $isChecked === null) { if (!$instanceId || !$taskCode || $isChecked === null) {
http_response_code(400); throw new WorkflowRuleFailedException('Missing required parameters: instance_id, task_code, is_checked');
echo json_encode(['error' => 'Missing required parameters: instance_id, task_code, is_checked']);
exit;
} }
try { $workflowEngine = new WorkflowEngine();
$pdo = db(); $result = $workflowEngine->updateChecklistStatus((int)$instanceId, $taskCode, (bool)$isChecked, (int)$userId);
echo json_encode($result);
// Get current data_json exit;
$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()]);
}
?>

View File

@ -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();
});
});

View File

@ -0,0 +1,26 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdo = db();
echo "Starting migration 021: Rename camelCase columns to snake_case...\n";
// Process instances table
$pdo->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());
}

View File

@ -0,0 +1,12 @@
<?php
require_once __DIR__ . '/../config.php';
try {
$pdo = db();
$sql = "ALTER TABLE process_definitions ADD COLUMN is_active TINYINT(1) NOT NULL DEFAULT 1 AFTER definition_json";
$pdo->exec($sql);
echo "Migration successful: is_active column added to process_definitions table.\n";
} catch (PDOException $e) {
die("Migration failed: " . $e->getMessage() . "\n");
}

View File

@ -9,16 +9,16 @@ try {
// 1. People table (unified table for users and contacts) // 1. People table (unified table for users and contacts)
$pdo->exec("CREATE TABLE IF NOT EXISTS `people` ( $pdo->exec("CREATE TABLE IF NOT EXISTS `people` (
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY, `id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`firstName` VARCHAR(255) NOT NULL, `first_name` VARCHAR(255) NOT NULL,
`lastName` VARCHAR(255) NOT NULL, `last_name` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL UNIQUE, `email` VARCHAR(255) NOT NULL UNIQUE,
`password` VARCHAR(255) NULL, `password` VARCHAR(255) NULL,
`companyName` VARCHAR(255) DEFAULT NULL, `company_name` VARCHAR(255) DEFAULT NULL,
`phone` VARCHAR(50) DEFAULT NULL, `phone` VARCHAR(50) DEFAULT NULL,
`role` ENUM('admin', 'team_member', 'member', 'guest') NOT NULL DEFAULT 'guest', `role` ENUM('admin', 'team_member', 'member', 'guest') NOT NULL DEFAULT 'guest',
`is_user` BOOLEAN NOT NULL DEFAULT FALSE, `is_user` BOOLEAN NOT NULL DEFAULT FALSE,
`active` BOOLEAN NOT NULL DEFAULT TRUE, `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"; echo "People table created or already exists.\n";
@ -28,7 +28,7 @@ try {
if ($stmt->fetchColumn() === false) { if ($stmt->fetchColumn() === false) {
$password = password_hash('password', PASSWORD_DEFAULT); $password = password_hash('password', PASSWORD_DEFAULT);
$insert_stmt = $pdo->prepare( $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]); $insert_stmt->execute(['admin@example.com', $password]);
echo "Default admin user created. Email: admin@example.com, Password: password\n"; echo "Default admin user created. Email: admin@example.com, Password: password\n";
@ -44,7 +44,7 @@ try {
`is_active` BOOLEAN NOT NULL DEFAULT TRUE, `is_active` BOOLEAN NOT NULL DEFAULT TRUE,
`start_node_id` VARCHAR(255), `start_node_id` VARCHAR(255),
`definition_json` TEXT, `definition_json` TEXT,
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)"); )");
echo "Process definitions table created or already exists.\n"; echo "Process definitions table created or already exists.\n";
@ -74,7 +74,7 @@ try {
`current_reason` TEXT, `current_reason` TEXT,
`suggested_next_step` TEXT, `suggested_next_step` TEXT,
`data_json` TEXT, `data_json` TEXT,
`lastActivityAt` TIMESTAMP NULL, `last_activity_at` TIMESTAMP NULL,
FOREIGN KEY (person_id) REFERENCES people(id) ON DELETE CASCADE, FOREIGN KEY (person_id) REFERENCES people(id) ON DELETE CASCADE,
FOREIGN KEY (process_definition_id) REFERENCES process_definitions(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`) UNIQUE KEY `person_process` (`person_id`, `process_definition_id`)
@ -84,15 +84,15 @@ try {
// 4. Process Events table (updated FK) // 4. Process Events table (updated FK)
$pdo->exec("CREATE TABLE IF NOT EXISTS `process_events` ( $pdo->exec("CREATE TABLE IF NOT EXISTS `process_events` (
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY, `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, `event_type` VARCHAR(50) NOT NULL,
`message` TEXT, `message` TEXT,
`node_id` VARCHAR(255), `node_id` VARCHAR(255),
`payload_json` TEXT, `payload_json` TEXT,
`createdBy` INT(11) UNSIGNED NOT NULL, `created_by` INT(11) UNSIGNED NOT NULL,
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (processInstanceId) REFERENCES process_instances(id) ON DELETE CASCADE, FOREIGN KEY (process_instance_id) REFERENCES process_instances(id) ON DELETE CASCADE,
FOREIGN KEY (createdBy) REFERENCES people(id) ON DELETE CASCADE FOREIGN KEY (created_by) REFERENCES people(id) ON DELETE CASCADE
)"); )");
echo "Process events table created or already exists.\n"; echo "Process events table created or already exists.\n";
@ -106,8 +106,8 @@ try {
if ($checkFk) { if ($checkFk) {
$pdo->exec("ALTER TABLE `process_instances` DROP FOREIGN KEY `{$checkFk['CONSTRAINT_NAME']}`;"); $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` CHANGE `processId` `process_definition_id` 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` ADD FOREIGN KEY (`process_definition_id`) REFERENCES `process_definitions`(`id`) ON DELETE CASCADE;");
echo "Migrated process_instances: processId -> processDefinitionId.\n"; echo "Migrated process_instances: processId -> processDefinitionId.\n";
} }
@ -118,7 +118,7 @@ try {
if ($checkFk) { if ($checkFk) {
$pdo->exec("ALTER TABLE `process_instances` DROP FOREIGN KEY `{$checkFk['CONSTRAINT_NAME']}`;"); $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"; echo "Migrated process_instances: contactId -> personId.\n";
} }

165
index.php
View File

@ -1,35 +1,25 @@
<?php <?php
session_start(); require_once 'db/config.php';
require_once 'WorkflowEngine.php'; require_once 'WorkflowEngine.php';
$workflowEngine = new WorkflowEngine();
$pdo = db(); $pdo = db();
// Fetch people with their group names $workflowEngine = new WorkflowEngine();
$stmt = $pdo->query(" $matrix = $workflowEngine->getDashboardMatrix();
SELECT p.*, bg.name as bni_group_name
FROM people p $people = $matrix['people'];
LEFT JOIN bni_groups bg ON p.bni_group_id = bg.id $instances = $matrix['instances'];
ORDER BY p.lastName, p.firstName $all_functions = $matrix['all_functions'];
"); $person_functions_map = $matrix['person_functions_map'];
$people = $stmt->fetchAll(PDO::FETCH_ASSOC); $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 = [ $status_colors = [
@ -43,23 +33,7 @@ $status_colors = [
'inactive' => '#808080', '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);
<div class="table-responsive"> <div class="table-responsive">
<?php <?php
// Fetch ID for the meeting process to make headers clickable // Find the meeting process ID from the already-fetched definitions
$stmt_meeting_process = $pdo->prepare("SELECT id FROM process_definitions WHERE name = ?"); $meeting_process_id = 'null';
$stmt_meeting_process->execute(['Przygotowanie spotkania grupy']); foreach ($matrix['definitions'] as $definition) {
$meeting_process = $stmt_meeting_process->fetch(PDO::FETCH_ASSOC); if ($definition['name'] === 'Przygotowanie spotkania grupy') {
$meeting_process_id = $meeting_process ? $meeting_process['id'] : 'null'; $meeting_process_id = $definition['id'];
break;
}
}
?> ?>
<table class="table table-bordered table-sm"> <table class="table table-bordered table-sm">
<thead class="table-light"> <thead class="table-light">
@ -181,9 +158,9 @@ $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
<td class="text-center align-middle"><input type="checkbox" class="person-checkbox" name="personIds[]" value="<?= $person['id'] ?>"></td> <td class="text-center align-middle"><input type="checkbox" class="person-checkbox" name="personIds[]" value="<?= $person['id'] ?>"></td>
<td class="person-cell"> <td class="person-cell">
<div class="person-main"> <div class="person-main">
<div class="person-name"><?= htmlspecialchars($person['firstName'] . ' ' . $person['lastName']) ?></div> <div class="person-name"><?= htmlspecialchars($person['first_name'] . ' ' . $person['last_name']) ?></div>
<div class="person-details"> <div class="person-details">
<span class="d-block"><?= htmlspecialchars($person['companyName']) ?></span> <span class="d-block"><?= htmlspecialchars($person['company_name']) ?></span>
<span class="d-block"><?= htmlspecialchars($person['industry'] ?? '') ?></span> <span class="d-block"><?= htmlspecialchars($person['industry'] ?? '') ?></span>
<span><?= htmlspecialchars(ucfirst($person['role'])) ?></span> <span><?= htmlspecialchars(ucfirst($person['role'])) ?></span>
<?php if ($person['role'] === 'member' && !empty($person['bni_group_name'])): ?> <?php if ($person['role'] === 'member' && !empty($person['bni_group_name'])): ?>
@ -201,7 +178,7 @@ $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
</div> </div>
<button class="btn btn-sm btn-secondary edit-btn" data-bs-toggle="modal" data-bs-target="#editPersonModal" <button class="btn btn-sm btn-secondary edit-btn" data-bs-toggle="modal" data-bs-target="#editPersonModal"
data-person-id="<?= $person['id'] ?>" data-person-id="<?= $person['id'] ?>"
data-person-name="<?= htmlspecialchars($person['firstName'] . ' ' . $person['lastName']) ?>"> data-person-name="<?= htmlspecialchars($person['first_name'] . ' ' . $person['last_name']) ?>">
Edit Edit
</button> </button>
</div> </div>
@ -227,51 +204,43 @@ $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
<?php // Inne Procesy Columns ?> <?php // Inne Procesy Columns ?>
<?php foreach ($inne_procesy_cols as $process): <?php foreach ($inne_procesy_cols as $process):
$instance = $instances[$person['id']][$process['id']] ?? null; $instance = $instances[$person['id']][$process['id']] ?? null;
$definition = isset($process['definition_json']) ? json_decode($process['definition_json'], true) : null; $lastActivity = $instance && isset($instance['last_activity_at']) ? date('d/m/y', strtotime($instance['last_activity_at'])) : '';
$isChecklist = ($definition && isset($definition['type']) && $definition['type'] === 'checklist');
$lastActivity = $instance && $instance['lastActivityAt'] ? date('d/m/y', strtotime($instance['lastActivityAt'])) : '';
$color = $status_colors['inactive']; $is_eligible = $instance ? ($instance['is_eligible'] ?? true) : false;
$title = 'Inactive';
if ($isChecklist) { if ($instance && isset($instance['id'])) { // Existing instance
$status = 'inactive'; $status = $instance['computed_status'];
if ($instance) {
$tasks = $definition['tasks'] ?? [];
$instanceData = $instance['data_json'] ? json_decode($instance['data_json'], true) : [];
$totalTasks = count($tasks);
$completedTasks = 0;
foreach ($tasks as $task) {
if (!empty($instanceData[$task['code']])) {
$completedTasks++;
}
}
$title = "$completedTasks/$totalTasks completed";
if ($totalTasks > 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'];
?>
<td class="align-middle checklist-cell text-center" style="cursor: pointer;" data-bs-toggle="modal" data-bs-target="#instanceModal" data-person-id="<?= $person['id'] ?>" data-process-id="<?= $process['id'] ?>" data-instance-id="<?= $instance['id'] ?? '' ?>">
<span title="<?= $title ?>" style="height: 20px; width: 20px; background-color: <?= $color ?>; border-radius: 50%; display: inline-block;"></span>
<small class="text-muted d-block last-activity-date mt-1"><?= $lastActivity ?></small>
</td>
<?php } else {
$status = $instance ? $instance['current_status'] : 'inactive';
$color = $status_colors[$status] ?? $status_colors['inactive']; $color = $status_colors[$status] ?? $status_colors['inactive'];
$title = ucfirst($status); $title = ucfirst($status);
?> if (!empty($instance['computed_reason'])) {
<td class="text-center align-middle" style="cursor: pointer;" data-bs-toggle="modal" data-bs-target="#instanceModal" data-person-id="<?= $person['id'] ?>" data-process-id="<?= $process['id'] ?>"> $title = $instance['computed_reason'];
<span title="<?= $title ?>" style="height: 20px; width: 20px; background-color: <?= $color ?>; border-radius: 50%; display: inline-block;"></span> }
<small class="text-muted d-block mt-1"><?= $lastActivity ?></small> $modal_target = '#instanceModal';
</td> } else { // No instance
<?php } if ($is_eligible) {
$status = 'not_started';
$color = $status_colors[$status];
$title = 'Not Started';
$modal_target = '#bulkInitModal';
} else {
$status = 'ineligible';
$color = '#e9ecef'; // A light gray color
$title = 'Not eligible';
$modal_target = ''; // Prevent modal
}
}
?>
<td class="text-center align-middle"
style="cursor: <?= $modal_target ? 'pointer' : 'not-allowed' ?>;"
<?= $modal_target ? 'data-bs-toggle="modal"' : '' ?>
data-bs-target="<?= $modal_target ?>"
data-person-id="<?= $person['id'] ?>"
data-process-id="<?= $process['id'] ?>"
title="<?= htmlspecialchars($title) ?>">
<span style="height: 20px; width: 20px; background-color: <?= $color ?>; border-radius: 50%; display: inline-block;"></span>
<small class="text-muted d-block mt-1"><?= $lastActivity ?></small>
</td>
<?php
endforeach; ?> endforeach; ?>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@ -301,11 +270,11 @@ $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
<div class="col-md-4"> <div class="col-md-4">
<div class="mb-3"> <div class="mb-3">
<label for="editFirstName" class="form-label">Imię</label> <label for="editFirstName" class="form-label">Imię</label>
<input type="text" class="form-control" id="editFirstName" name="firstName" required> <input type="text" class="form-control" id="editFirstName" name="first_name" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="editLastName" class="form-label">Nazwisko</label> <label for="editLastName" class="form-label">Nazwisko</label>
<input type="text" class="form-control" id="editLastName" name="lastName" required> <input type="text" class="form-control" id="editLastName" name="last_name" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="editPhone" class="form-label">Numer telefonu</label> <label for="editPhone" class="form-label">Numer telefonu</label>
@ -348,7 +317,7 @@ $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="editCompanyName" class="form-label">Nazwa firmy</label> <label for="editCompanyName" class="form-label">Nazwa firmy</label>
<input type="text" class="form-control" id="editCompanyName" name="companyName"> <input type="text" class="form-control" id="editCompanyName" name="company_name">
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
@ -429,11 +398,11 @@ $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
<div class="col-md-4"> <div class="col-md-4">
<div class="mb-3"> <div class="mb-3">
<label for="createFirstName" class="form-label">First Name <span class="text-danger">*</span></label> <label for="createFirstName" class="form-label">First Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="createFirstName" name="firstName" required> <input type="text" class="form-control" id="createFirstName" name="first_name" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="createLastName" class="form-label">Last Name <span class="text-danger">*</span></label> <label for="createLastName" class="form-label">Last Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="createLastName" name="lastName" required> <input type="text" class="form-control" id="createLastName" name="last_name" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="createPhone" class="form-label">Phone Number</label> <label for="createPhone" class="form-label">Phone Number</label>
@ -477,7 +446,7 @@ $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="createCompanyName" class="form-label">Company Name</label> <label for="createCompanyName" class="form-label">Company Name</label>
<input type="text" class="form-control" id="createCompanyName" name="companyName"> <input type="text" class="form-control" id="createCompanyName" name="company_name">
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
@ -641,7 +610,7 @@ document.addEventListener('DOMContentLoaded', function () {
const modalBody = instanceModal.querySelector('.modal-body'); const modalBody = instanceModal.querySelector('.modal-body');
const modalTitle = instanceModal.querySelector('.modal-title'); 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(response => response.text())
.then(html => { .then(html => {
modalBody.innerHTML = html; modalBody.innerHTML = html;
@ -902,12 +871,12 @@ document.addEventListener('DOMContentLoaded', function () {
tableHtml += '<tbody>'; tableHtml += '<tbody>';
instances.forEach(instance => { 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; if (!person) return;
tableHtml += `<tr><td class="person-cell"> tableHtml += `<tr><td class="person-cell">
<div class="person-main"> <div class="person-main">
<div class="person-name">${htmlspecialchars(person.firstName + ' ' + person.lastName)}</div> <div class="person-name">${htmlspecialchars(person.first_name + ' ' + person.last_name)}</div>
</div> </div>
</td>`; </td>`;

49
lib/ErrorHandler.php Normal file
View File

@ -0,0 +1,49 @@
<?php
require_once __DIR__ . '/WorkflowExceptions.php';
function generate_correlation_id() {
return uniqid('corr_');
}
function handle_exception(Throwable $e, $correlation_id) {
if ($e instanceof WorkflowException) {
http_response_code($e->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
});
}

View File

@ -0,0 +1,44 @@
<?php
class WorkflowException extends Exception {
protected $httpCode;
protected $details;
public function __construct($message = "", $code = 0, $httpCode = 500, $details = [], Throwable $previous = null) {
parent::__construct($message, $code, $previous);
$this->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);
}
}

View File

@ -21,7 +21,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$_SESSION['user_id'] = $person['id']; $_SESSION['user_id'] = $person['id'];
$_SESSION['user_email'] = $person['email']; $_SESSION['user_email'] = $person['email'];
$_SESSION['user_role'] = $person['role']; $_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'); header('Location: index.php');
exit; exit;
} else { } else {

View File

@ -101,11 +101,22 @@ $processes = $stmt->fetchAll(PDO::FETCH_ASSOC);
<div class="col-5"> <div class="col-5">
<select id="toStatusSelect" class="form-select"></select> <select id="toStatusSelect" class="form-select"></select>
</div> </div>
</div>
<div class="mb-3">
<label for="transitionActionJson" class="form-label">Actions (JSON)</label>
<textarea class="form-control" id="transitionActionJson" rows="2"></textarea>
</div> </div>
<div class="d-grid gap-2 mt-3"> <div class="d-grid gap-2 mt-3">
<button class="btn btn-outline-secondary" type="button" id="addTransitionBtn">Add Transition</button> <button class="btn btn-outline-secondary" type="button" id="addTransitionBtn">Add Transition</button>
</div> </div>
<hr>
<h5>Eligibility Rules (JSON)</h5>
<div class="mb-3">
<textarea class="form-control" id="eligibilityRulesJson" rows="3"></textarea>
</div>
<textarea name="definition_json" id="definitionJson" class="d-none"></textarea> <textarea name="definition_json" id="definitionJson" class="d-none"></textarea>
<hr class="mt-4"> <hr class="mt-4">
@ -119,97 +130,4 @@ $processes = $stmt->fetchAll(PDO::FETCH_ASSOC);
<?php include '_footer.php'; ?> <?php include '_footer.php'; ?>
<script> <script src="assets/js/process_definitions.js?v=<?php echo time(); ?>"></script>
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');
function renderProcessDefinition(definition) {
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) {
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';
form.action = '_save_process_definition.php';
processIdInput.value = processId;
processNameInput.value = processName;
definitionJsonTextarea.value = processDefinition;
renderProcessDefinition(processDefinition);
} else {
modalTitle.textContent = 'Create Process';
form.action = '_save_process_definition.php';
processIdInput.value = '';
processNameInput.value = '';
definitionJsonTextarea.value = '';
renderProcessDefinition(''); // Clear the form
}
});
});
</script>