Podział na procesy indywidualne i grupowe

This commit is contained in:
Flatlogic Bot 2026-03-02 10:47:44 +00:00
parent 9134470c19
commit 9ea7b9d268
9 changed files with 339 additions and 242 deletions

View File

@ -173,13 +173,50 @@ class WorkflowEngine {
$stmt_meetings->execute(['today' => $today]);
$upcoming_meetings_flat = $stmt_meetings->fetchAll(PDO::FETCH_ASSOC);
$spotkania_cols = [];
$spotkania_cols = [];
$meeting_ids = [];
foreach ($upcoming_meetings_flat as $meeting) {
$meetingId = $this->getOrCreateMeeting($meeting['group_id'], $meeting['start_datetime']);
$meeting_ids[] = $meetingId;
$spotkania_cols[$meeting['group_id']]['group_id'] = $meeting['group_id'];
$spotkania_cols[$meeting['group_id']]['group_name'] = $meeting['group_name'];
$spotkania_cols[$meeting['group_id']]['meetings'][] = $meeting['start_datetime'];
$spotkania_cols[$meeting['group_id']]['meetings'][] = [
'datetime' => $meeting['start_datetime'],
'meeting_id' => $meetingId
];
}
// Fetch process states for these meetings
$meeting_processes = [];
if (!empty($meeting_ids)) {
$placeholders = implode(',', array_fill(0, count($meeting_ids), '?'));
$stmt_mp = $this->pdo->prepare(
"SELECT pi.*, pd.code as process_code, pd.definition_json as pd_definition_json
FROM process_instances pi
JOIN process_definitions pd ON pi.process_definition_id = pd.id
WHERE pi.subject_type = 'meeting' AND pi.subject_id IN ($placeholders)"
);
$stmt_mp->execute($meeting_ids);
$mp_data = $stmt_mp->fetchAll(\PDO::FETCH_ASSOC);
foreach ($mp_data as $instance) {
$def_id = $instance['process_definition_id'];
$node_id = $instance['current_node_id'];
$definition = !empty($instance['pd_definition_json']) ? json_decode($instance['pd_definition_json'], true) : ($definition_map[$def_id] ?? null);
if ($definition && isset($definition['nodes'][$node_id])) {
$node_info = $definition['nodes'][$node_id];
$instance['computed_status'] = $node_info['ui_hints']['status'] ?? $instance['current_status'];
} else {
$instance['computed_status'] = $instance['current_status'];
}
$meeting_processes[$instance['subject_id']][$instance['process_code']] = $instance;
}
}
return [
'people' => $people,
@ -188,7 +225,8 @@ class WorkflowEngine {
'all_functions' => $all_functions,
'person_functions_map' => $person_functions_map,
'bni_groups' => $bni_groups,
'spotkania_cols' => $spotkania_cols, // Add this to the return array
'spotkania_cols' => $spotkania_cols,
'meeting_processes' => $meeting_processes, // Add this to the return array
];
}
@ -234,9 +272,9 @@ class WorkflowEngine {
}
$stmt_insert = $this->pdo->prepare(
"INSERT INTO process_instances (person_id, process_definition_id, current_node_id, current_status, last_activity_at, data_json) VALUES (?, ?, ?, 'in_progress', NOW(), ?)"
"INSERT INTO process_instances (person_id, subject_type, subject_id, process_definition_id, current_node_id, current_status, last_activity_at, data_json) VALUES (?, 'person', ?, ?, ?, 'in_progress', NOW(), ?)"
);
$stmt_insert->execute([$personId, $definition['id'], $startNodeId, $initialDataJson]);
$stmt_insert->execute([$personId, $personId, $definition['id'], $startNodeId, $initialDataJson]);
$instanceId = $this->pdo->lastInsertId();
// 3. Create a system event for process start.
@ -556,9 +594,9 @@ class WorkflowEngine {
$data_json = !empty($initial_data_map) ? json_encode($initial_data_map) : null;
$stmt = $this->pdo->prepare(
"INSERT INTO process_instances (person_id, process_definition_id, current_node_id, current_status, data_json, last_activity_at) VALUES (?, ?, ?, 'in_progress', ?, NOW())"
"INSERT INTO process_instances (person_id, subject_type, subject_id, process_definition_id, current_node_id, current_status, data_json, last_activity_at) VALUES (?, 'person', ?, ?, ?, 'in_progress', ?, NOW())"
);
$stmt->execute([$personId, $processDefinitionId, $start_node, $data_json]);
$stmt->execute([$personId, $personId, $processDefinitionId, $start_node, $data_json]);
$newInstanceId = $this->pdo->lastInsertId();
$this->logEvent($newInstanceId, 'process_started', 'Process started', $start_node, ['context' => $context], $userId);
@ -990,4 +1028,129 @@ class WorkflowEngine {
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
public function startProcessForSubject(string $processCode, string $subjectType, int $subjectId, int $userId, string $mode='resume_or_create', ?string $cycleKey=null): int {
$inTransaction = $this->pdo->inTransaction();
if (!$inTransaction) { $this->pdo->beginTransaction(); }
try {
$stmt_def = $this->pdo->prepare("SELECT * FROM process_definitions WHERE code = ? AND is_active = 1");
$stmt_def->execute([$processCode]);
$definition = $stmt_def->fetch(PDO::FETCH_ASSOC);
if (!$definition) {
// If no active definition by code, try by ID if it's a checklist fallback
$stmt_def = $this->pdo->prepare("SELECT * FROM process_definitions WHERE id = ?");
$stmt_def->execute([$processCode]);
$definition = $stmt_def->fetch(PDO::FETCH_ASSOC);
if (!$definition) {
throw new WorkflowNotFoundException("Process definition '$processCode' not found.");
}
$definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : [];
if (empty($definition_json) || $definition_json['type'] !== 'checklist') {
throw new WorkflowNotAllowedException("Process definition '$processCode' not found or not a checklist.");
}
$startNodeId = null;
} else {
$definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : [];
if (empty($definition_json) || !isset($definition_json['start_node_id'])) {
throw new WorkflowRuleFailedException("Process definition is missing start_node_id.");
}
$startNodeId = $definition_json['start_node_id'];
}
$initialDataJson = null;
if (isset($definition_json['initial_data'])) {
$initialDataJson = json_encode($definition_json['initial_data']);
}
// Map person_id for backward compatibility
$personId = ($subjectType === 'person') ? $subjectId : 0;
$stmt_insert = $this->pdo->prepare(
"INSERT INTO process_instances (person_id, subject_type, subject_id, cycle_key, process_definition_id, current_node_id, current_status, last_activity_at, data_json) VALUES (?, ?, ?, ?, ?, ?, 'in_progress', NOW(), ?)"
);
$stmt_insert->execute([$personId, $subjectType, $subjectId, $cycleKey, $definition['id'], $startNodeId, $initialDataJson]);
$instanceId = $this->pdo->lastInsertId();
$this->addEvent($instanceId, 'system', 'Process started.', $startNodeId, [], $userId);
if (!$inTransaction) { $this->pdo->commit(); }
return (int)$instanceId;
} catch (Exception $e) {
if (!$inTransaction) { $this->pdo->rollBack(); }
throw $e;
}
}
public function getOrCreateInstanceForSubject(int $processDefinitionId, string $subjectType, int $subjectId, int $userId, string $mode='resume_or_create', ?string $cycleKey=null): array {
$stmt_def = $this->pdo->prepare("SELECT code, is_active FROM process_definitions WHERE id = ?");
$stmt_def->execute([$processDefinitionId]);
$definition = $stmt_def->fetch(PDO::FETCH_ASSOC);
if (!$definition) {
throw new WorkflowNotFoundException("Process definition #$processDefinitionId not found.");
}
$code = $definition['code'];
$instance = null;
if ($mode !== 'create_new_run') {
$instance = $this->getActiveInstanceForSubject($code, $subjectType, $subjectId, $cycleKey);
}
if (!$instance) {
if (empty($definition['is_active'])) {
throw new WorkflowNotAllowedException("Process is not active and cannot be started.");
}
$instanceId = $this->startProcessForSubject($code, $subjectType, $subjectId, $userId, $mode, $cycleKey);
$stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE id = ?");
$stmt->execute([$instanceId]);
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
}
return $instance;
}
public function getActiveInstanceForSubject(string $processCode, string $subjectType, int $subjectId, ?string $cycleKey=null): ?array {
$sql = "
SELECT pi.*
FROM process_instances pi
JOIN process_definitions pd ON pi.process_definition_id = pd.id
WHERE pi.subject_type = ? AND pi.subject_id = ? AND pd.code = ?
";
$params = [$subjectType, $subjectId, $processCode];
if ($cycleKey !== null) {
$sql .= " AND pi.cycle_key = ?";
$params[] = $cycleKey;
}
$sql .= " ORDER BY pi.last_activity_at DESC, pi.id DESC LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
return $instance ?: null;
}
public function getInstancesHistoryForSubject(string $processCode, string $subjectType, int $subjectId, int $limit=5): array {
$sql = "
SELECT pi.*
FROM process_instances pi
JOIN process_definitions pd ON pi.process_definition_id = pd.id
WHERE pi.subject_type = ? AND pi.subject_id = ? AND pd.code = ?
ORDER BY pi.last_activity_at DESC, pi.id DESC
LIMIT ?
";
$stmt = $this->pdo->prepare($sql);
$stmt->bindValue(1, $subjectType, PDO::PARAM_STR);
$stmt->bindValue(2, $subjectId, PDO::PARAM_INT);
$stmt->bindValue(3, $processCode, PDO::PARAM_STR);
$stmt->bindValue(4, $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}

View File

@ -25,38 +25,51 @@ if (!$process_definition_id && $process_code) {
$process_definition_id = $stmt->fetchColumn();
}
if (!$person_id || !$process_definition_id) {
if (!$subject_id || !$process_definition_id) {
http_response_code(400);
echo json_encode(['error' => 'Missing person_id or process_id']);
echo json_encode(['error' => 'Missing subject_id or process_id']);
exit;
}
$userId = $_SESSION['user_id'];
$engine = new WorkflowEngine();
// Fetch Person and Process Definition details first
$stmt_person = $pdo->prepare("SELECT first_name, last_name FROM people WHERE id = ?");
$stmt_person->execute([$person_id]);
$person = $stmt_person->fetch();
// Fetch Subject and Process Definition details first
$subjectName = 'Unknown';
if ($subject_type === 'person') {
$stmt_person = $pdo->prepare("SELECT first_name, last_name FROM people WHERE id = ?");
$stmt_person->execute([$subject_id]);
$person = $stmt_person->fetch();
if ($person) {
$subjectName = $person['first_name'] . ' ' . $person['last_name'];
}
} elseif ($subject_type === 'meeting') {
$stmt_meeting = $pdo->prepare("SELECT m.meeting_datetime, bg.name as group_name FROM meetings m JOIN bni_groups bg ON m.bni_group_id = bg.id WHERE m.id = ?");
$stmt_meeting->execute([$subject_id]);
$meeting = $stmt_meeting->fetch();
if ($meeting) {
$subjectName = $meeting['group_name'] . ' - ' . date('d.m.Y', strtotime($meeting['meeting_datetime']));
}
}
$stmt_process = $pdo->prepare("SELECT * FROM process_definitions WHERE id = ?");
$stmt_process->execute([$process_definition_id]);
$process = $stmt_process->fetch();
if (!$person || !$process) {
if (!$process) {
http_response_code(404);
echo "<p class='text-danger'>Could not find person or process.</p>";
echo "<p class='text-danger'>Could not find process.</p>";
exit;
}
// Try to find an existing instance
$instance = $engine->getInstanceByDefId($person_id, $process_definition_id);
$instance = $engine->getActiveInstanceForSubject($process['code'], $subject_type, $subject_id);
?>
<!-- Title for the modal, to be grabbed by JS -->
<div id="instance-modal-title" class="d-none">
<?= htmlspecialchars($person['first_name']." ".$person['last_name']) ?> - <?= htmlspecialchars($process['name']) ?>
<?= htmlspecialchars($subjectName) ?> - <?= htmlspecialchars($process['name']) ?>
</div>
<?php if ($instance): // INSTANCE EXISTS ?>
@ -70,7 +83,7 @@ $instance = $engine->getInstanceByDefId($person_id, $process_definition_id);
<strong><?= t('modal.process_completed', 'Proces zakończony.') ?></strong>
Obecny status: <?= htmlspecialchars($instance['current_status']) ?>.
</div>
<button id="restartProcessBtn" class="btn btn-sm btn-primary" data-person-id="<?= $person_id ?>" data-process-code="<?= htmlspecialchars($process['code']) ?>" data-mode="create_new_run">
<button id="restartProcessBtn" class="btn btn-sm btn-primary" data-subject-type="<?= $subject_type ?>" data-subject-id="<?= $subject_id ?>" data-person-id="<?= $person_id ?>" data-process-code="<?= htmlspecialchars($process['code']) ?>" data-mode="create_new_run">
<?= t('modal.start_new_instance', 'Rozpocznij od nowa') ?>
</button>
</div>
@ -266,14 +279,17 @@ $instance = $engine->getInstanceByDefId($person_id, $process_definition_id);
<?php else: // NO INSTANCE EXISTS ?>
<?php
$eligibility = $engine->checkEligibility($person_id, $process_definition_id);
$eligibility = ['is_eligible' => true, 'reasons' => []];
if ($subject_type === 'person') {
$eligibility = $engine->checkEligibility($subject_id, $process_definition_id);
}
?>
<div class="text-center">
<?php if ($eligibility['is_eligible']): ?>
<h4><?= t('modal.process_not_started', 'Proces nie został uruchomiony') ?></h4>
<p>This process has not been started for this person.</p>
<button id="startProcessBtn" class="btn btn-primary" data-person-id="<?= $person_id ?>" data-process-id="<?= $process_definition_id ?>">
<button id="startProcessBtn" class="btn btn-primary" data-subject-type="<?= $subject_type ?>" data-subject-id="<?= $subject_id ?>" data-person-id="<?= $person_id ?>" data-process-id="<?= $process_definition_id ?>">
<?= t('modal.start_process', 'Uruchom proces') ?>
</button>
<?php else: ?>

View File

@ -19,6 +19,8 @@ if (!isset($_SESSION['user_id'])) {
$userId = $_SESSION['user_id'];
$personId = filter_input(INPUT_POST, 'person_id', FILTER_VALIDATE_INT);
$subjectType = filter_input(INPUT_POST, 'subject_type', FILTER_SANITIZE_STRING) ?: 'person';
$subjectId = filter_input(INPUT_POST, 'subject_id', FILTER_VALIDATE_INT) ?: $personId;
$processDefinitionId = filter_input(INPUT_POST, 'process_id', FILTER_VALIDATE_INT);
$processCode = filter_input(INPUT_POST, 'process_code', FILTER_SANITIZE_STRING);
$deleteExisting = filter_input(INPUT_POST, 'delete_existing');
@ -32,23 +34,32 @@ if (!$processDefinitionId && $processCode) {
$processDefinitionId = $stmt->fetchColumn();
}
if (!$personId || !$processDefinitionId) {
throw new InvalidArgumentException('Invalid or missing person_id or process_id/process_code.');
if (!$subjectId || !$processDefinitionId) {
throw new InvalidArgumentException('Invalid or missing subject_id or process_id/process_code.');
}
$engine = new WorkflowEngine();
$stmt_def = $pdo->prepare("SELECT code FROM process_definitions WHERE id = ?");
$stmt_def->execute([$processDefinitionId]);
$code = $stmt_def->fetchColumn() ?: $processCode;
if($deleteExisting === '1') {
$instance = $engine->getInstanceByDefId($personId, $processDefinitionId);
$instance = $engine->getActiveInstanceForSubject($code, $subjectType, $subjectId);
if ($instance) {
$engine->deleteInstance($instance['id']);
}
}
if ($mode === 'create_new_run' || filter_input(INPUT_POST, 'force', FILTER_VALIDATE_INT) === 1) {
$instance = $engine->createNewInstance($personId, $processDefinitionId, $userId);
// start new process
$instanceId = $engine->startProcessForSubject($code, $subjectType, $subjectId, $userId, 'create_new_run');
$stmt = $pdo->prepare("SELECT * FROM process_instances WHERE id = ?");
$stmt->execute([$instanceId]);
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
} else {
$instance = $engine->getOrCreateInstanceByDefId($personId, $processDefinitionId, $userId);
$instance = $engine->getOrCreateInstanceForSubject($processDefinitionId, $subjectType, $subjectId, $userId);
}
if ($instance) {

View File

@ -0,0 +1,66 @@
<?php
function migrate_037($pdo) {
echo "Starting migration 037 (Process Subjects)...
";
// 1. process_instances: add subject_type, subject_id, cycle_key, bni_group_id
$stmt = $pdo->query("SHOW COLUMNS FROM process_instances LIKE 'subject_type'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD COLUMN subject_type VARCHAR(20) NOT NULL DEFAULT 'person' AFTER person_id");
echo "Added subject_type to process_instances.
";
}
$stmt = $pdo->query("SHOW COLUMNS FROM process_instances LIKE 'subject_id'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD COLUMN subject_id INT NOT NULL DEFAULT 0 AFTER subject_type");
echo "Added subject_id to process_instances.
";
}
$stmt = $pdo->query("SHOW COLUMNS FROM process_instances LIKE 'cycle_key'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD COLUMN cycle_key VARCHAR(64) NULL AFTER subject_id");
echo "Added cycle_key to process_instances.
";
}
$stmt = $pdo->query("SHOW COLUMNS FROM process_instances LIKE 'bni_group_id'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD COLUMN bni_group_id INT NULL AFTER cycle_key");
echo "Added bni_group_id to process_instances.
";
}
// 2. Data migration
echo "Migrating existing data...
";
$pdo->exec("UPDATE process_instances SET subject_type = 'person', subject_id = person_id WHERE subject_id = 0");
// 3. Add indexes
$stmt = $pdo->query("SHOW INDEX FROM process_instances WHERE Key_name = 'idx_subject_active'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD INDEX idx_subject_active (subject_type, subject_id, current_status)");
echo "Added index idx_subject_active.
";
}
$stmt = $pdo->query("SHOW INDEX FROM process_instances WHERE Key_name = 'idx_subject_cycle'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD INDEX idx_subject_cycle (subject_type, subject_id, cycle_key)");
echo "Added index idx_subject_cycle.
";
}
$stmt = $pdo->query("SHOW INDEX FROM process_instances WHERE Key_name = 'idx_group'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD INDEX idx_group (bni_group_id)");
echo "Added index idx_group.
";
}
echo "Migration 037 completed.
";
}

View File

@ -120,7 +120,9 @@ $status_colors = [
<?php
// Define process groups
// Show all processes from the DB directly.
$inne_procesy_cols = $processes;
$inne_procesy_cols = array_filter($processes, function($p) {
return $p['code'] !== 'meeting_preparation';
});
?>
@ -143,16 +145,25 @@ $status_colors = [
<tr class="text-center">
<th rowspan="2" class="align-middle"><input type="checkbox" id="selectAll"></th>
<th rowspan="2" class="align-middle"><?= t('dashboard.person', 'Osoba') ?></th>
<?php
$mp_def = null;
foreach ($definitions as $def) {
if ($def['code'] === 'meeting_preparation') {
$mp_def = $def;
break;
}
}
?>
<?php if (!empty($spotkania_cols)): ?>
<th colspan="<?= count($spotkania_cols) ?>"><?= t('dashboard.meetings', 'Spotkania') ?></th>
<?php endif; ?>
<?php if (!empty($inne_procesy_cols)): ?>
<th colspan="<?= count($processes) ?>"><?= t('dashboard.other_processes', 'Inne procesy') ?></th>
<th colspan="<?= count($inne_procesy_cols) ?>"><?= t('dashboard.other_processes', 'Inne procesy') ?></th>
<?php endif; ?>
</tr>
<tr class="text-center">
<?php foreach ($spotkania_cols as $col):
$isMeetingFilterActive = ($meetingFilterGroupId == $col['group_id'] && $meetingFilterDatetime == ($col['meetings'][0] ?? null));
$isMeetingFilterActive = ($meetingFilterGroupId == $col['group_id'] && $meetingFilterDatetime == ($col['meetings'][0]['datetime'] ?? null));
?>
<th data-group-id="<?= $col['group_id'] ?>" data-meetings='<?= json_encode($col['meetings'] ?? []) ?>' class="<?= $isMeetingFilterActive ? 'active-filter' : '' ?>">
<a href="#" class="text-decoration-none text-dark meeting-filter-link">
@ -161,13 +172,24 @@ $status_colors = [
<div class="mx-2 text-center">
<?= htmlspecialchars($col['group_name']) ?><br>
<small class="meeting-date"></small>
<?php if ($mp_def): ?>
<div class="mt-1 meeting-process-container" style="display:none;">
<span class="badge rounded-circle bg-secondary process-dot meeting-process-dot"
style="width: 15px; height: 15px; display: inline-block; cursor: pointer;"
data-bs-toggle="modal" data-bs-target="#instanceModal"
data-process-id="<?= $mp_def['id'] ?>"
data-subject-type="meeting"
data-subject-id=""
title="Przygotowanie spotkania"></span>
</div>
<?php endif; ?>
</div>
<button class="btn btn-sm btn-outline-secondary meeting-nav-btn meeting-next-btn"><i class="bi bi-chevron-right"></i></button>
</div>
</a>
</th>
<?php endforeach; ?>
<?php foreach ($processes as $col): ?>
<?php foreach ($inne_procesy_cols as $col): ?>
<?php
$filterParams = $_GET;
$filterParams['active_process_id'] = $col['id'];
@ -222,7 +244,7 @@ $status_colors = [
// The meeting date will be determined by JS, we will add it to the data attribute
// The initial meeting date is the first one in the list.
$meeting_datetime = $col['meetings'][0] ?? '';
$meeting_datetime = $col['meetings'][0]['datetime'] ?? '';
$color = $status_colors[$status] ?? 'secondary';
?>
@ -238,7 +260,7 @@ $status_colors = [
<?php endforeach; ?>
<?php // Inne Procesy Columns ?>
<?php foreach ($processes as $process):
<?php foreach ($inne_procesy_cols as $process):
$instance = $instances[$person['id']][$process['id']] ?? null;
$status = $instance ? $instance['computed_status'] : 'none';
$color = $status_colors[$status] ?? 'secondary';
@ -694,6 +716,10 @@ $status_colors = [
<script>
document.addEventListener('DOMContentLoaded', function () {
const meetingProcesses = <?= json_encode($meeting_processes ?? []) ?>;
const statusColors = <?= json_encode($status_colors ?? []) ?>;
const meetingModal = new bootstrap.Modal(document.getElementById('meetingAttendanceModal'));
const meetingForm = document.getElementById('meetingAttendanceForm');
const meetingPersonName = document.getElementById('meetingPersonName');
@ -857,8 +883,26 @@ document.addEventListener('DOMContentLoaded', function () {
function updateMeetingView() {
if (meetings.length > 0) {
const currentMeetingDate = meetings[currentIndex];
const currentMeetingObj = meetings[currentIndex];
const currentMeetingDate = typeof currentMeetingObj === 'object' ? currentMeetingObj.datetime : currentMeetingObj;
const currentMeetingId = typeof currentMeetingObj === 'object' ? currentMeetingObj.meeting_id : null;
headerCell.querySelector('.meeting-date').textContent = new Date(currentMeetingDate).toLocaleDateString('pl-PL', { day: '2-digit', month: '2-digit', year: 'numeric' });
const processContainer = headerCell.querySelector('.meeting-process-container');
if (processContainer && currentMeetingId) {
processContainer.style.display = 'block';
const dot = processContainer.querySelector('.meeting-process-dot');
if (dot) {
dot.dataset.subjectId = currentMeetingId;
const instance = meetingProcesses[currentMeetingId] && meetingProcesses[currentMeetingId]['meeting_preparation'];
const status = instance ? instance.computed_status : 'none';
const color = statusColors[status] || 'secondary';
dot.className = `badge rounded-circle bg-${color} process-dot meeting-process-dot`;
dot.dataset.processId = instance ? instance.id : '';
}
} else if (processContainer) {
processContainer.style.display = 'none';
}
fetchAndUpdateAllDotsForGroup(groupId, currentMeetingDate);
} else {
headerCell.querySelector('.meeting-date').textContent = 'Brak';
@ -923,12 +967,14 @@ document.addEventListener('DOMContentLoaded', function () {
var instanceModal = document.getElementById('instanceModal');
instanceModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var personId = button.dataset.personId;
var personId = button.dataset.personId || '';
var processId = button.dataset.processId;
var subjectType = button.dataset.subjectType || 'person';
var subjectId = button.dataset.subjectId || personId;
var modalBody = instanceModal.querySelector('.modal-body');
// Load content via AJAX
fetch(`_get_instance_details.php?person_id=${personId}&process_id=${processId}`)
fetch(`_get_instance_details.php?person_id=${personId}&subject_type=${subjectType}&subject_id=${subjectId}&process_id=${processId}`)
.then(response => response.text())
.then(html => {
modalBody.innerHTML = html;
@ -1459,6 +1505,8 @@ document.addEventListener('DOMContentLoaded', function () {
if(button && button.dataset) {
if (button.dataset.personId) instanceModalElement.dataset.lastPersonId = button.dataset.personId;
if (button.dataset.processId) instanceModalElement.dataset.lastProcessId = button.dataset.processId;
if (button.dataset.subjectType) instanceModalElement.dataset.lastSubjectType = button.dataset.subjectType;
if (button.dataset.subjectId) instanceModalElement.dataset.lastSubjectId = button.dataset.subjectId;
}
});
}

View File

@ -1,71 +0,0 @@
import re
with open('WorkflowEngine.php', 'r') as f:
content = f.read()
content = content.replace(
'SELECT id, name, definition_json, is_active FROM process_definitions WHERE is_active = 1 ORDER BY sort_order, name',
'SELECT id, code, name, definition_json, is_active FROM process_definitions WHERE is_active = 1 AND is_latest = 1 ORDER BY sort_order, name'
)
content = content.replace(
"'name' => $def['name'],",
"'code' => $def['code'],\n 'name' => $def['name'],"
)
content = content.replace(
'$stmt_instances = $this->pdo->prepare("SELECT * FROM process__instances WHERE person_id IN ($placeholders)");\n $stmt_instances->execute($person_ids);\n $instances_data = $stmt_instances->fetchAll(PDO::FETCH_ASSOC);',
"""
$stmt_instances = $this->pdo->prepare(
SELECT pi.*, pd.code as process_code, pd.id as definition_actual_id
FROM process_instances pi
JOIN process_definitions pd ON pi.process_definition_id = pd.id
WHERE pi.person_id IN ($placeholders)
ORDER BY pi.last_activity_at DESC, pi.id DESC
");
$stmt_instances->execute($person_ids);
$instances_data = $stmt_instances->fetchAll(PDO::FETCH_ASSOC);
// Map code to latest definition id
$codeToIdMap = [];
foreach ($definitions as $defId => $def) {
$codeToIdMap[$def['code']] = $defId;
}""")
content = content.replace(
"$instances[$instance['person_id']][$def_id] = $enriched_instance;",
"""
// Use process_code to map to the latest active column
$code = $instance['process_code'] ?? null;
if ($code && isset($codeToIdMap[$code])) {
$latestDefId = $codeToIdMap[$code];
// Only keep the most recently active instance per code (since ordered by last_activity_at DESC)
if (!isset($instances[$instance['person_id']][$latestDefId])) {
$instances[$instance['person_id']][$latestDefId] = $enriched_instance;
}
}""")
content = re.sub(
r"public function getInstanceByDefId\(int \$personId, int \$processDefinitionId\): \?array \{.*?return \$instance \?: null;\s*\}",
"""public function getInstanceByDefId(int $personId, int $processDefinitionId): ?array {
$stmt_code = $this->pdo->prepare("SELECT code FROM process_definitions WHERE id = ?");
$stmt_code->execute([$processDefinitionId]);
$code = $stmt_code->fetchColumn();
if (!$code) return null;
$stmt = $this->pdo->prepare("\n SELECT pi.* \n FROM process_instances pi\n JOIN process_definitions pd ON pi.process_definition_id = pd.id\n WHERE pi.person_id = ? AND pd.code = ?\n ORDER BY pi.last_activity_at DESC, pi.id DESC\n LIMIT 1\n ");
$stmt->execute([$personId, $code]);
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
return $instance ?: null;
}""",
content, flags=re.DOTALL
)
# Wait, checkEligibility needs to use the NEW version definition ID?
# Actually checkEligibility uses $processDefinitionId which IS the new version's ID!
with open('WorkflowEngine.php', 'w') as f:
f.write(content)
print("Patched.")

View File

@ -1,59 +0,0 @@
import re
with open('WorkflowEngine.php', 'r') as f:
content = f.read()
new_method = """
public function createNewInstance(int $personId, int $processDefinitionId, int $userId, array $context = []): array {
if (!is_int($processDefinitionId) || $processDefinitionId <= 0) {
throw new InvalidArgumentException("processDefinitionId must be a positive integer.");
}
if (!is_int($personId) || $personId <= 0) {
throw new InvalidArgumentException("personId must be a positive integer.");
}
$stmt_def = $this->pdo->prepare("SELECT definition_json, code, is_active FROM process_definitions WHERE id = ?");
$stmt_def->execute([$processDefinitionId]);
$definition = $stmt_def->fetch(PDO::FETCH_ASSOC);
if (!$definition) {
throw new WorkflowNotFoundException("Process definition #$processDefinitionId not found.");
}
if (empty($definition['is_active'])) {
throw new WorkflowNotAllowedException("Process is not active and cannot be started.");
}
$eligibility = $this->checkEligibility($personId, $processDefinitionId, $context);
if (!$eligibility['is_eligible']) {
throw new WorkflowEligibilityException("Person is not eligible to start this process.", $eligibility['reasons']);
}
$definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : [];
$start_node = $definition_json['start_node_id'] ?? 'start';
$initial_data_map = $definition_json['initial_data'] ?? [];
$data_json = !empty($initial_data_map) ? json_encode($initial_data_map) : null;
$stmt = $this->pdo->prepare(
"INSERT INTO process_instances (person_id, process_definition_id, current_node_id, current_status, data_json, last_activity_at) VALUES (?, ?, ?, 'in_progress', ?, NOW())"
);
$stmt->execute([$personId, $processDefinitionId, $start_node, $data_json]);
$newInstanceId = $this->pdo->lastInsertId();
$this->logEvent($newInstanceId, 'process_started', 'Process started', $start_node, ['context' => $context], $userId);
$stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE id = ?");
$stmt->execute([$newInstanceId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
"
content = content.replace(
'public function getOrCreateInstanceByDefId(int $personId, int $processDefinitionId, int $userId, array $context = []): ?array {',
new_method + '\n public function getOrCreateInstanceByDefId(int $personId, int $processDefinitionId, int $userId, array $context = []): ?array {'
)
with open('WorkflowEngine.php', 'w') as f:
f.write(content)
print("Patched.")

View File

@ -1,17 +0,0 @@
import re
with open('_get_instance_details.php', 'r', encoding='utf-8') as f:
content = f.read()
pattern = r'<button id="startNewProcessBtn"([^>]+)>.*?</button>\s*</div>\s*<script>.*?</script>'
replacement = r'''<button id="restartProcessBtn" class="btn btn-sm btn-primary" data-person-id="<?= $person_id ?>" data-process-code="<?= htmlspecialchars($process['code']) ?>" data-mode="create_new_run">
<?= t('modal.start_new_instance', 'Rozpocznij od nowa') ?>
</button>
</div>'''
new_content, count = re.subn(pattern, replacement, content, flags=re.DOTALL)
with open('_get_instance_details.php', 'w', encoding='utf-8') as f:
f.write(new_content)
print(f"Replaced {count} instances.")

View File

@ -1,60 +0,0 @@
import re
with open('_save_process_definition.php', 'r') as f:
content = f.read()
old_update_logic = """ if (empty($processId)) {
// Create new process
$sql = 'INSERT INTO process_definitions (name, code, definition_json, start_node_id, is_active) VALUES (?, ?, ?, ?, 1)';
$params = [$name, $code, $definition_json, $start_node];
$message = 'Process created successfully.';
} else {
// Update existing process
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 0;
$sql = 'UPDATE process_definitions SET name = ?, definition_json = ?, start_node_id = ?, is_active = ? WHERE id = ?';
$params = [$name, $definition_json, $start_node, $is_active, $processId];
$message = 'Process updated successfully.';
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);"""
new_update_logic = """ if (empty($processId)) {
// Create new process
$sql = 'INSERT INTO process_definitions (name, code, definition_json, start_node_id, is_active, version, is_latest) VALUES (?, ?, ?, ?, 1, 1, 1)';
$params = [$name, $code, $definition_json, $start_node];
$message = 'Process created successfully.';
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
} else {
// "Update" existing process by creating a new version
$stmt_old = $pdo->prepare('SELECT code, version, sort_order, is_active FROM process_definitions WHERE id = ?');
$stmt_old->execute([$processId]);
$old = $stmt_old->fetch();
if ($old) {
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : $old['is_active'];
$new_version = $old['version'] + 1;
$db_code = $old['code'];
// Mark all previous versions as not latest
$stmt_update = $pdo->prepare('UPDATE process_definitions SET is_latest = 0 WHERE code = ?');
$stmt_update->execute([$db_code]);
// Insert new version
$sql = 'INSERT INTO process_definitions (name, code, definition_json, start_node_id, is_active, version, supersedes_definition_id, is_latest, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?)';
$params = [$name, $db_code, $definition_json, $start_node, $is_active, $new_version, $processId, $old['sort_order']];
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$message = 'Process updated successfully (new version created).';
} else {
throw new WorkflowRuleFailedException('Process not found.');
}
}"""
content = content.replace(old_update_logic, new_update_logic)
with open('_save_process_definition.php', 'w') as f:
f.write(content)
print("Patched.")