Duże poprawki
This commit is contained in:
parent
6460ff3ac8
commit
44d4fa5a60
@ -11,19 +11,45 @@ class WorkflowEngine {
|
|||||||
$this->pdo = db();
|
$this->pdo = db();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDashboardMatrix(): array {
|
public function getDashboardMatrix(?string $searchTerm = null, ?int $groupId = null, ?int $activeProcessDefinitionId = null): array {
|
||||||
// Get all people (potential assignees)
|
// 1. Base query for people
|
||||||
$stmt_people = $this->pdo->prepare("
|
$sql_people = "SELECT p.*, bg.name as bni_group_name FROM people p LEFT JOIN bni_groups bg ON p.bni_group_id = bg.id";
|
||||||
SELECT p.*, bg.name as bni_group_name
|
$params = [];
|
||||||
FROM people p
|
$where_clauses = [];
|
||||||
LEFT JOIN bni_groups bg ON p.bni_group_id = bg.id
|
|
||||||
ORDER BY p.last_name, p.first_name
|
// 2. Add filter conditions
|
||||||
");
|
if ($searchTerm) {
|
||||||
$stmt_people->execute();
|
$where_clauses[] = "(p.first_name LIKE :search OR p.last_name LIKE :search OR p.company_name LIKE :search OR p.email LIKE :search)";
|
||||||
|
$params[':search'] = '%' . $searchTerm . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($groupId) {
|
||||||
|
$where_clauses[] = "p.bni_group_id = :group_id";
|
||||||
|
$params[':group_id'] = $groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($activeProcessDefinitionId) {
|
||||||
|
$terminal_statuses = ['positive', 'negative', 'completed', 'error', 'inactive'];
|
||||||
|
$in_clause = implode(',', array_map([$this->pdo, 'quote'], $terminal_statuses));
|
||||||
|
|
||||||
|
$sql_people .= " INNER JOIN process_instances pi ON p.id = pi.person_id";
|
||||||
|
$where_clauses[] = "pi.process_definition_id = :active_process_id AND (pi.current_status IS NOT NULL AND pi.current_status NOT IN ($in_clause))";
|
||||||
|
$params[':active_process_id'] = $activeProcessDefinitionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($where_clauses)) {
|
||||||
|
$sql_people .= " WHERE " . implode(" AND ", $where_clauses);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql_people .= " ORDER BY p.last_name, p.first_name";
|
||||||
|
|
||||||
|
// 3. Execute query to get filtered people
|
||||||
|
$stmt_people = $this->pdo->prepare($sql_people);
|
||||||
|
$stmt_people->execute($params);
|
||||||
$people = $stmt_people->fetchAll(PDO::FETCH_ASSOC);
|
$people = $stmt_people->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// Fetch all process definitions with their JSON
|
// 4. Fetch all process definitions with their JSON
|
||||||
$stmt_defs = $this->pdo->prepare("SELECT id, name, definition_json FROM process_definitions ORDER BY name");
|
$stmt_defs = $this->pdo->prepare("SELECT id, name, definition_json, is_active FROM process_definitions WHERE is_active = 1 ORDER BY sort_order, name");
|
||||||
$stmt_defs->execute();
|
$stmt_defs->execute();
|
||||||
$process_definitions_raw = $stmt_defs->fetchAll(PDO::FETCH_ASSOC);
|
$process_definitions_raw = $stmt_defs->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
@ -38,12 +64,15 @@ class WorkflowEngine {
|
|||||||
$definition_map[$def['id']] = !empty($def['definition_json']) ? json_decode($def['definition_json'], true) : null;
|
$definition_map[$def['id']] = !empty($def['definition_json']) ? json_decode($def['definition_json'], true) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch instances
|
// 5. Fetch instances ONLY for the filtered people
|
||||||
$stmt_instances = $this->pdo->prepare("SELECT * FROM process_instances");
|
$instances = [];
|
||||||
$stmt_instances->execute();
|
$person_ids = array_column($people, 'id');
|
||||||
|
if (!empty($person_ids)) {
|
||||||
|
$placeholders = implode(',', array_fill(0, count($person_ids), '?'));
|
||||||
|
$stmt_instances = $this->pdo->prepare("SELECT * FROM process_instances WHERE person_id IN ($placeholders)");
|
||||||
|
$stmt_instances->execute($person_ids);
|
||||||
$instances_data = $stmt_instances->fetchAll(PDO::FETCH_ASSOC);
|
$instances_data = $stmt_instances->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$instances = [];
|
|
||||||
foreach ($instances_data as $instance) {
|
foreach ($instances_data as $instance) {
|
||||||
$enriched_instance = $instance;
|
$enriched_instance = $instance;
|
||||||
$def_id = $instance['process_definition_id'];
|
$def_id = $instance['process_definition_id'];
|
||||||
@ -64,7 +93,6 @@ class WorkflowEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if ($totalTasks > 0 && $completedTasks === $totalTasks) {
|
if ($totalTasks > 0 && $completedTasks === $totalTasks) {
|
||||||
$status = 'completed';
|
$status = 'completed';
|
||||||
} elseif ($completedTasks > 0) {
|
} elseif ($completedTasks > 0) {
|
||||||
@ -88,21 +116,9 @@ class WorkflowEngine {
|
|||||||
|
|
||||||
$instances[$instance['person_id']][$def_id] = $enriched_instance;
|
$instances[$instance['person_id']][$def_id] = $enriched_instance;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove pre-emptive eligibility check. This is now handled on-demand by _get_instance_details.php
|
// 6. Fetch ancillary data
|
||||||
/*
|
|
||||||
foreach ($people as $person) {
|
|
||||||
foreach ($definitions as $def) {
|
|
||||||
if (!isset($instances[$person['id']][$def['id']])) {
|
|
||||||
$process_definition_raw = $process_definitions_raw[array_search($def['id'], array_column($process_definitions_raw, 'id'))];
|
|
||||||
$eligibility = $this->checkEligibility($person['id'], $process_definition_raw);
|
|
||||||
$instances[$person['id']][$def['id']] = ['is_eligible' => $eligibility['is_eligible']];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Fetch ancillary data
|
|
||||||
$stmt_functions = $this->pdo->query("SELECT * FROM functions ORDER BY display_order");
|
$stmt_functions = $this->pdo->query("SELECT * FROM functions ORDER BY display_order");
|
||||||
$all_functions = $stmt_functions->fetchAll(PDO::FETCH_ASSOC);
|
$all_functions = $stmt_functions->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
@ -115,6 +131,18 @@ class WorkflowEngine {
|
|||||||
$stmt_bni_groups = $this->pdo->query("SELECT * FROM bni_groups ORDER BY name");
|
$stmt_bni_groups = $this->pdo->query("SELECT * FROM bni_groups ORDER BY name");
|
||||||
$bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
|
$bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 7. Fetch Spotkania columns (upcoming meetings)
|
||||||
|
$today = date('Y-m-d H:i:s');
|
||||||
|
$stmt_meetings = $this->pdo->prepare("
|
||||||
|
SELECT bni_groups.id as group_id, bni_groups.name as group_name, MIN(calendar_events.start_datetime) as next_meeting_date
|
||||||
|
FROM bni_groups
|
||||||
|
LEFT JOIN calendar_event_groups ON bni_groups.id = calendar_event_groups.bni_group_id
|
||||||
|
LEFT JOIN calendar_events ON calendar_event_groups.calendar_event_id = calendar_events.id AND calendar_events.start_datetime >= :today
|
||||||
|
GROUP BY bni_groups.id
|
||||||
|
ORDER BY bni_groups.name
|
||||||
|
");
|
||||||
|
$stmt_meetings->execute(['today' => $today]);
|
||||||
|
$spotkania_cols = $stmt_meetings->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'people' => $people,
|
'people' => $people,
|
||||||
@ -123,6 +151,7 @@ class WorkflowEngine {
|
|||||||
'all_functions' => $all_functions,
|
'all_functions' => $all_functions,
|
||||||
'person_functions_map' => $person_functions_map,
|
'person_functions_map' => $person_functions_map,
|
||||||
'bni_groups' => $bni_groups,
|
'bni_groups' => $bni_groups,
|
||||||
|
'spotkania_cols' => $spotkania_cols, // Add this to the return array
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -698,4 +727,22 @@ class WorkflowEngine {
|
|||||||
// Also update the in-memory instance for the next step in the chain
|
// Also update the in-memory instance for the next step in the chain
|
||||||
$instance['data_json'] = $newDataJson;
|
$instance['data_json'] = $newDataJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteInstance(int $instanceId): void {
|
||||||
|
$this->pdo->beginTransaction();
|
||||||
|
try {
|
||||||
|
// Delete events
|
||||||
|
$stmt_events = $this->pdo->prepare("DELETE FROM process_events WHERE process_instance_id = ?");
|
||||||
|
$stmt_events->execute([$instanceId]);
|
||||||
|
|
||||||
|
// Delete instance
|
||||||
|
$stmt_instance = $this->pdo->prepare("DELETE FROM process_instances WHERE id = ?");
|
||||||
|
$stmt_instance->execute([$instanceId]);
|
||||||
|
|
||||||
|
$this->pdo->commit();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->pdo->rollBack();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -102,6 +102,7 @@ $instance = $engine->getInstanceByDefId($person_id, $process_definition_id);
|
|||||||
<h5>Kroki procesu</h5>
|
<h5>Kroki procesu</h5>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
<?php foreach ($all_nodes as $nodeId => $node):
|
<?php foreach ($all_nodes as $nodeId => $node):
|
||||||
|
if (!isset($node['ui_hints']['title']) || $node['ui_hints']['title'] === '') continue;
|
||||||
$is_current = ($currentNodeId === $nodeId);
|
$is_current = ($currentNodeId === $nodeId);
|
||||||
$is_completed = isset($visited_nodes[$nodeId]) && !$is_current;
|
$is_completed = isset($visited_nodes[$nodeId]) && !$is_current;
|
||||||
|
|
||||||
@ -122,7 +123,7 @@ $instance = $engine->getInstanceByDefId($person_id, $process_definition_id);
|
|||||||
<li class="list-group-item d-flex justify-content-between align-items-center <?= $li_class ?>">
|
<li class="list-group-item d-flex justify-content-between align-items-center <?= $li_class ?>">
|
||||||
<div>
|
<div>
|
||||||
<?= $status_icon ?>
|
<?= $status_icon ?>
|
||||||
<strong><?= htmlspecialchars($node['name']) ?></strong>
|
<strong><?= htmlspecialchars($node['ui_hints']['title']) ?></strong>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@ -139,8 +140,14 @@ $instance = $engine->getInstanceByDefId($person_id, $process_definition_id);
|
|||||||
<label for="<?= $field['name'] ?>" class="form-label"><?= $field['label'] ?></label>
|
<label for="<?= $field['name'] ?>" class="form-label"><?= $field['label'] ?></label>
|
||||||
<?php if ($field['type'] === 'textarea'): ?>
|
<?php if ($field['type'] === 'textarea'): ?>
|
||||||
<textarea id="<?= $field['name'] ?>" name="<?= $field['name'] ?>" class="form-control"></textarea>
|
<textarea id="<?= $field['name'] ?>" name="<?= $field['name'] ?>" class="form-control"></textarea>
|
||||||
|
<?php elseif ($field['type'] === 'select'): ?>
|
||||||
|
<select id="<?= $field['name'] ?>" name="<?= $field['name'] ?>" class="form-select">
|
||||||
|
<?php foreach ($field['options'] as $option): ?>
|
||||||
|
<option value="<?= $option['value'] ?>"><?= $option['label'] ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<input type="<?= $field['type'] ?>" id="<?= $field['name'] ?>" name="<?= $field['name'] ?>" class="form-control" value="<?= $field['default'] === 'now' ? date('Y-m-d\\TH:i') : '' ?>">
|
<input type="<?= $field['type'] ?>" id="<?= $field['name'] ?>" name="<?= $field['name'] ?>" class="form-control" value="<?= ($field['default'] ?? '') === 'now' ? date('Y-m-d\\TH:i') : '' ?>">
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@ -178,18 +185,56 @@ $instance = $engine->getInstanceByDefId($person_id, $process_definition_id);
|
|||||||
<?php if (empty($events)): ?>
|
<?php if (empty($events)): ?>
|
||||||
<p>Brak zdarzeń.</p>
|
<p>Brak zdarzeń.</p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
|
<?php
|
||||||
|
// Prepare a map for outcome_status labels for readability
|
||||||
|
$outcomeStatusOptions = [];
|
||||||
|
if (isset($definition['nodes']['awaiting_call']['ui_hints']['form_schema'])) {
|
||||||
|
foreach ($definition['nodes']['awaiting_call']['ui_hints']['form_schema'] as $field) {
|
||||||
|
if ($field['name'] === 'outcome_status' && isset($field['options'])) {
|
||||||
|
foreach ($field['options'] as $option) {
|
||||||
|
if (!empty($option['value'])) {
|
||||||
|
$outcomeStatusOptions[$option['value']] = $option['label'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
<?php foreach ($events as $event): ?>
|
<?php foreach ($events as $event): ?>
|
||||||
|
<?php
|
||||||
|
$payload = json_decode($event['payload_json'], true);
|
||||||
|
$isCallAttempt = $event['event_type'] === 'transition_completed' && isset($payload['transition_id']) && $payload['transition_id'] === 'submit_outcome';
|
||||||
|
?>
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
|
<?php if ($isCallAttempt): ?>
|
||||||
|
<?php
|
||||||
|
$data = $payload['data'] ?? [];
|
||||||
|
$outcomeLabel = $outcomeStatusOptions[$data['outcome_status']] ?? ucfirst(str_replace('_', ' ', $data['outcome_status']));
|
||||||
|
?>
|
||||||
|
<div class="d-flex w-100 justify-content-between">
|
||||||
|
<h6 class="mb-1">Call Attempt</h6>
|
||||||
|
<small><?= date('d.m.Y, H:i', strtotime($data['call_date'])) ?></small>
|
||||||
|
</div>
|
||||||
|
<p class="mb-1"><strong>Outcome:</strong> <?= htmlspecialchars($outcomeLabel) ?></p>
|
||||||
|
<?php if (!empty($data['note'])): ?>
|
||||||
|
<p class="mb-1 fst-italic">"<?= nl2br(htmlspecialchars($data['note'])) ?>"</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($data['next_contact_date'])): ?>
|
||||||
|
<p class="mb-1"><strong>Next follow-up:</strong> <?= date('d.m.Y, H:i', strtotime($data['next_contact_date'])) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php else: // Generic event display ?>
|
||||||
<strong><?= htmlspecialchars(ucfirst(str_replace('_', ' ', $event['event_type']))) ?></strong>
|
<strong><?= htmlspecialchars(ucfirst(str_replace('_', ' ', $event['event_type']))) ?></strong>
|
||||||
<?php
|
<?php
|
||||||
if (!empty($event['message'])) {
|
if (!empty($event['message'])) {
|
||||||
$payload = json_decode($event['payload_json'], true);
|
|
||||||
$message = $payload['message'] ?? $event['message'];
|
$message = $payload['message'] ?? $event['message'];
|
||||||
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['first_name'] . ' ' . $event['last_name']) ?> dnia <?= date('d.m.Y, H:i', strtotime($event['created_at'])) ?></small>
|
<?php endif; ?>
|
||||||
|
<small class="text-muted">By <?= htmlspecialchars($event['first_name'] . ' ' . $event['last_name']) ?> on <?= date('d.m.Y, H:i', strtotime($event['created_at'])) ?></small>
|
||||||
</li>
|
</li>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
<?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');
|
||||||
|
|||||||
@ -20,6 +20,7 @@ if (!isset($_SESSION['user_id'])) {
|
|||||||
$userId = $_SESSION['user_id'];
|
$userId = $_SESSION['user_id'];
|
||||||
$personId = filter_input(INPUT_POST, 'person_id', FILTER_VALIDATE_INT);
|
$personId = filter_input(INPUT_POST, 'person_id', FILTER_VALIDATE_INT);
|
||||||
$processDefinitionId = filter_input(INPUT_POST, 'process_id', FILTER_VALIDATE_INT);
|
$processDefinitionId = filter_input(INPUT_POST, 'process_id', FILTER_VALIDATE_INT);
|
||||||
|
$deleteExisting = filter_input(INPUT_POST, 'delete_existing');
|
||||||
|
|
||||||
if (!$personId || !$processDefinitionId) {
|
if (!$personId || !$processDefinitionId) {
|
||||||
// InvalidArgumentException will be caught by the handler and result in a 400 Bad Request
|
// InvalidArgumentException will be caught by the handler and result in a 400 Bad Request
|
||||||
@ -28,6 +29,13 @@ if (!$personId || !$processDefinitionId) {
|
|||||||
|
|
||||||
$engine = new WorkflowEngine();
|
$engine = new WorkflowEngine();
|
||||||
|
|
||||||
|
if($deleteExisting === '1') {
|
||||||
|
$instance = $engine->getInstanceByDefId($personId, $processDefinitionId);
|
||||||
|
if ($instance) {
|
||||||
|
$engine->deleteInstance($instance['id']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The getOrCreateInstanceByDefId method is now responsible for all checks:
|
// The getOrCreateInstanceByDefId method is now responsible for all checks:
|
||||||
// 1. Validating the process definition exists.
|
// 1. Validating the process definition exists.
|
||||||
// 2. Checking if the process is active.
|
// 2. Checking if the process is active.
|
||||||
|
|||||||
@ -16,7 +16,7 @@ function validate_definition_json($json) {
|
|||||||
throw new WorkflowRuleFailedException('Invalid JSON format in definition.');
|
throw new WorkflowRuleFailedException('Invalid JSON format in definition.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$allowed_statuses = ['none', 'negative', 'in_progress', 'positive'];
|
$allowed_statuses = ['none', 'negative', 'in_progress', 'positive', 'active', 'processing', 'paused', 'completed', 'terminated'];
|
||||||
|
|
||||||
if (isset($data['nodes'])) {
|
if (isset($data['nodes'])) {
|
||||||
foreach ($data['nodes'] as $node) {
|
foreach ($data['nodes'] as $node) {
|
||||||
@ -75,8 +75,8 @@ try {
|
|||||||
} else {
|
} else {
|
||||||
// Update existing process
|
// Update existing process
|
||||||
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 0;
|
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 0;
|
||||||
$sql = 'UPDATE process_definitions SET name = ?, code = ?, definition_json = ?, is_active = ? WHERE id = ?';
|
$sql = 'UPDATE process_definitions SET name = ?, definition_json = ?, is_active = ? WHERE id = ?';
|
||||||
$params = [$name, $code, $definition_json, $is_active, $processId];
|
$params = [$name, $definition_json, $is_active, $processId];
|
||||||
$message = 'Process updated successfully.';
|
$message = 'Process updated successfully.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,4 +2,4 @@
|
|||||||
# https://curl.se/docs/http-cookies.html
|
# https://curl.se/docs/http-cookies.html
|
||||||
# This file was generated by libcurl! Edit at your own risk.
|
# This file was generated by libcurl! Edit at your own risk.
|
||||||
|
|
||||||
127.0.0.1 FALSE / FALSE 0 PHPSESSID abf9a8g0sidv8idojcrr65jetp
|
localhost FALSE / FALSE 0 PHPSESSID rfo6k0p8l4tpnmgek7dkpkopbl
|
||||||
|
|||||||
130
current_definition.json
Normal file
130
current_definition.json
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
{
|
||||||
|
"start_node_id": "awaiting_call",
|
||||||
|
"eligibility_rules": [
|
||||||
|
{
|
||||||
|
"type": "person_property_equals",
|
||||||
|
"params": {
|
||||||
|
"property": "role",
|
||||||
|
"value": "guest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nodes": {
|
||||||
|
"awaiting_call": {
|
||||||
|
"ui_hints": {
|
||||||
|
"title": "Follow-up Call",
|
||||||
|
"status": "active",
|
||||||
|
"reason": "Awaiting follow-up call with the guest.",
|
||||||
|
"next_step": "Log the outcome of the call.",
|
||||||
|
"form_schema": [
|
||||||
|
{ "name": "call_date", "label": "Call Date", "type": "datetime-local", "default": "now", "required": true },
|
||||||
|
{
|
||||||
|
"name": "outcome_status",
|
||||||
|
"label": "Call Outcome",
|
||||||
|
"type": "select",
|
||||||
|
"required": true,
|
||||||
|
"options": [
|
||||||
|
{ "value": "", "label": "-- Select Outcome --" },
|
||||||
|
{ "value": "no_answer", "label": "No Answer" },
|
||||||
|
{ "value": "wants_to_join", "label": "Wants to Join" },
|
||||||
|
{ "value": "declined", "label": "Declined" },
|
||||||
|
{ "value": "call_later", "label": "Call Later" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "name": "note", "label": "Notes", "type": "textarea" },
|
||||||
|
{ "name": "next_contact_date", "label": "Next Contact Date", "type": "datetime-local", "condition": { "field": "outcome_status", "value": "call_later" }, "required": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outcome_router": { "ui_hints": { "status": "processing" } },
|
||||||
|
"waiting_for_next_contact": {
|
||||||
|
"ui_hints": {
|
||||||
|
"title": "Waiting for Scheduled Call",
|
||||||
|
"status": "paused",
|
||||||
|
"reason": "Waiting until the scheduled date for the next call.",
|
||||||
|
"next_step": "Resume contact on or after the scheduled date."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"decide_after_no_answer": {
|
||||||
|
"ui_hints": {
|
||||||
|
"title": "No Answer",
|
||||||
|
"status": "paused",
|
||||||
|
"reason": "The guest did not answer the call.",
|
||||||
|
"next_step": "Decide whether to try again or end the process."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"end_positive": { "ui_hints": { "title": "Wants to Join", "status": "completed", "reason": "Guest wants to join. New member process started.", "next_step": "" } },
|
||||||
|
"end_negative_declined": { "ui_hints": { "title": "Declined", "status": "terminated", "reason": "Guest declined to join.", "next_step": "" } },
|
||||||
|
"end_negative_no_answer": { "ui_hints": { "title": "Process Ended", "status": "terminated", "reason": "Process ended after no answer.", "next_step": "" } },
|
||||||
|
"end_negative_terminated": { "ui_hints": { "title": "Process Terminated", "status": "terminated", "reason": "Process manually terminated by user.", "next_step": "" } }
|
||||||
|
},
|
||||||
|
"transitions": [
|
||||||
|
{
|
||||||
|
"id": "submit_outcome",
|
||||||
|
"from": "awaiting_call",
|
||||||
|
"to": "outcome_router",
|
||||||
|
"name": "Submit Outcome",
|
||||||
|
"actions": [
|
||||||
|
{ "type": "set_data", "params": { "keys": ["call_date", "outcome_status", "note", "next_contact_date"] } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "route_wants_to_join",
|
||||||
|
"from": "outcome_router",
|
||||||
|
"to": "end_positive",
|
||||||
|
"name": "Route to Positive End",
|
||||||
|
"condition": { "field": "outcome_status", "value": "wants_to_join" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "start_process", "process_code": "obsluga-przyjecia-nowego-czlonka" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "route_declined",
|
||||||
|
"from": "outcome_router",
|
||||||
|
"to": "end_negative_declined",
|
||||||
|
"name": "Route to Declined",
|
||||||
|
"condition": { "field": "outcome_status", "value": "declined" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "route_no_answer",
|
||||||
|
"from": "outcome_router",
|
||||||
|
"to": "decide_after_no_answer",
|
||||||
|
"name": "Route to No Answer",
|
||||||
|
"condition": { "field": "outcome_status", "value": "no_answer" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "route_call_later",
|
||||||
|
"from": "outcome_router",
|
||||||
|
"to": "waiting_for_next_contact",
|
||||||
|
"name": "Route to Call Later",
|
||||||
|
"condition": { "field": "outcome_status", "value": "call_later" }
|
||||||
|
},
|
||||||
|
{ "id": "continue_attempts", "from": "decide_after_no_answer", "to": "awaiting_call", "name": "Try Again" },
|
||||||
|
{ "id": "end_attempts", "from": "decide_after_no_answer", "to": "end_negative_no_answer", "name": "End Process" },
|
||||||
|
{ "id": "resume_contact", "from": "waiting_for_next_contact", "to": "awaiting_call", "name": "Resume / Attempt Call" },
|
||||||
|
{
|
||||||
|
"id": "terminate_from_awaiting_call",
|
||||||
|
"from": "awaiting_call",
|
||||||
|
"to": "end_negative_terminated",
|
||||||
|
"name": "Zakończ proces",
|
||||||
|
"actions": [ { "type": "set_data", "params": { "keys": ["termination_note"] } } ],
|
||||||
|
"form_schema": [ { "name": "termination_note", "label": "Reason for Termination (optional)", "type": "textarea" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "terminate_from_decide",
|
||||||
|
"from": "decide_after_no_answer",
|
||||||
|
"to": "end_negative_terminated",
|
||||||
|
"name": "Zakończ proces",
|
||||||
|
"actions": [ { "type": "set_data", "params": { "keys": ["termination_note"] } } ],
|
||||||
|
"form_schema": [ { "name": "termination_note", "label": "Reason for Termination (optional)", "type": "textarea" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "terminate_from_waiting",
|
||||||
|
"from": "waiting_for_next_contact",
|
||||||
|
"to": "end_negative_terminated",
|
||||||
|
"name": "Zakończ proces",
|
||||||
|
"actions": [ { "type": "set_data", "params": { "keys": ["termination_note"] } } ],
|
||||||
|
"form_schema": [ { "name": "termination_note", "label": "Reason for Termination (optional)", "type": "textarea" } ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
11
db/migrations/026_simplify_follow_up_ux.php
Normal file
11
db/migrations/026_simplify_follow_up_ux.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../../db/config.php';
|
||||||
|
|
||||||
|
$db = db();
|
||||||
|
|
||||||
|
$new_json_definition = file_get_contents(__DIR__ . '/../../new_definition.json');
|
||||||
|
|
||||||
|
$stmt = $db->prepare("UPDATE process_definitions SET definition_json = :json WHERE id = 4");
|
||||||
|
$stmt->execute(['json' => $new_json_definition]);
|
||||||
|
|
||||||
|
echo "Process definition for follow_up (ID: 4) updated successfully.";
|
||||||
19
db/migrations/027_deactivate_sales_pipeline.php
Normal file
19
db/migrations/027_deactivate_sales_pipeline.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
|
||||||
|
function migrate_027($pdo) {
|
||||||
|
try {
|
||||||
|
$sql = "UPDATE process_definitions SET is_active = 0 WHERE name = 'Sales Pipeline'";
|
||||||
|
$pdo->exec($sql);
|
||||||
|
echo "Migration successful: Deactivated 'Sales Pipeline' process.\n";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("Migration failed: " . $e->getMessage() . "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) {
|
||||||
|
require_once __DIR__ . '/../../db/config.php';
|
||||||
|
$pdo = db();
|
||||||
|
migrate_027($pdo);
|
||||||
|
}
|
||||||
|
|
||||||
22
db/migrations/028_add_sort_order_to_process_definitions.php
Normal file
22
db/migrations/028_add_sort_order_to_process_definitions.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
|
||||||
|
function migrate_028($pdo) {
|
||||||
|
try {
|
||||||
|
$sql = "ALTER TABLE process_definitions ADD COLUMN sort_order INT NOT NULL DEFAULT 0 AFTER is_active";
|
||||||
|
$pdo->exec($sql);
|
||||||
|
echo "Migration successful: sort_order column added to process_definitions table.\n";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Ignore if column already exists
|
||||||
|
if (strpos($e->getMessage(), 'Duplicate column name') === false) {
|
||||||
|
die("Migration failed: " . $e->getMessage() . "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) {
|
||||||
|
require_once __DIR__ . '/../../db/config.php';
|
||||||
|
$pdo = db();
|
||||||
|
migrate_028($pdo);
|
||||||
|
}
|
||||||
|
|
||||||
32
db/migrations/029_set_initial_process_order.php
Normal file
32
db/migrations/029_set_initial_process_order.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
|
||||||
|
function migrate_029($pdo) {
|
||||||
|
$process_order = [
|
||||||
|
'Przygotowanie spotkania grupy' => 10,
|
||||||
|
'Follow-up' => 20,
|
||||||
|
'Obsługa przyjęcia nowego członka' => 30,
|
||||||
|
'Wprowadzenie nowego członka' => 40,
|
||||||
|
'Szkolenia dla młodego członka' => 50,
|
||||||
|
'Mentoring' => 60,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("UPDATE process_definitions SET sort_order = :sort_order WHERE name = :name");
|
||||||
|
|
||||||
|
foreach ($process_order as $name => $order) {
|
||||||
|
$stmt->execute(['sort_order' => $order, 'name' => $name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Migration successful: Initial process order set.\n";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("Migration failed: " . $e->getMessage() . "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) {
|
||||||
|
require_once __DIR__ . '/../../db/config.php';
|
||||||
|
$pdo = db();
|
||||||
|
migrate_029($pdo);
|
||||||
|
}
|
||||||
|
|
||||||
7
get_definition.php
Normal file
7
get_definition.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
require 'db/config.php';
|
||||||
|
$db = db();
|
||||||
|
$stmt = $db->prepare("SELECT definition_json FROM process_definitions WHERE id = 4");
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
echo $result['definition_json'];
|
||||||
29
get_process_definitions.php
Normal file
29
get_process_definitions.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->query("SELECT id, code, name, is_active FROM process_definitions ORDER BY id");
|
||||||
|
$definitions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Identify which processes appear on the dashboard
|
||||||
|
foreach ($definitions as &$def) {
|
||||||
|
if (!in_array($def['name'], ['Obsluga goscia', 'Przygotowanie spotkania grupy'])) {
|
||||||
|
$def['on_dashboard'] = 'Yes';
|
||||||
|
} else {
|
||||||
|
$def['on_dashboard'] = 'No';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "--- Process Definitions ---
|
||||||
|
";
|
||||||
|
echo str_pad("ID", 5) . str_pad("Code", 40) . str_pad("Name", 40) . str_pad("Active", 10) . str_pad("On Dashboard", 15) . "\n";
|
||||||
|
echo str_repeat("-", 110) . "\n";
|
||||||
|
foreach ($definitions as $def) {
|
||||||
|
echo str_pad($def['id'], 5) . str_pad($def['code'], 40) . str_pad($def['name'], 40) . str_pad($def['is_active'] ? 'Yes' : 'No', 10) . str_pad($def['on_dashboard'], 15) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("DB ERROR: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
786
index.php
786
index.php
@ -1,41 +1,50 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once 'db/config.php';
|
|
||||||
require_once 'WorkflowEngine.php';
|
require_once 'WorkflowEngine.php';
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
$workflowEngine = new WorkflowEngine();
|
$workflowEngine = new WorkflowEngine();
|
||||||
$matrix = $workflowEngine->getDashboardMatrix();
|
|
||||||
|
|
||||||
$people = $matrix['people'];
|
$searchTerm = $_GET['search'] ?? null;
|
||||||
$instances = $matrix['instances'];
|
$groupId = isset($_GET['group_id']) && $_GET['group_id'] !== '' ? (int)$_GET['group_id'] : null;
|
||||||
$all_functions = $matrix['all_functions'];
|
$activeProcessId = isset($_GET['active_process_id']) && $_GET['active_process_id'] !== '' ? (int)$_GET['active_process_id'] : null;
|
||||||
$person_functions_map = $matrix['person_functions_map'];
|
|
||||||
$bni_groups = $matrix['bni_groups'];
|
|
||||||
|
|
||||||
// Filter out specific process definitions
|
|
||||||
$processes = array_filter($matrix['definitions'], function($process) {
|
|
||||||
return !in_array($process['name'], ['Obsluga goscia', 'Przygotowanie spotkania grupy']);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Handle toggling of the active process filter
|
||||||
|
if (isset($_GET['active_process_id']) && $_GET['active_process_id'] == @$_SESSION['last_active_process_id']) {
|
||||||
|
unset($_GET['active_process_id']);
|
||||||
|
$activeProcessId = null;
|
||||||
|
$redirectUrl = "index.php";
|
||||||
|
$queryParams = [];
|
||||||
|
if ($groupId) {
|
||||||
|
$queryParams['group_id'] = $groupId;
|
||||||
|
}
|
||||||
|
if ($searchTerm) {
|
||||||
|
$queryParams['search'] = $searchTerm;
|
||||||
|
}
|
||||||
|
if (!empty($queryParams)) {
|
||||||
|
$redirectUrl .= "?" . http_build_query($queryParams);
|
||||||
|
}
|
||||||
|
header("Location: " . $redirectUrl);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$_SESSION['last_active_process_id'] = $activeProcessId;
|
||||||
|
|
||||||
|
|
||||||
|
$matrixData = $workflowEngine->getDashboardMatrix($searchTerm, $groupId, $activeProcessId);
|
||||||
|
$people = $matrixData['people'];
|
||||||
|
$processes = $matrixData['definitions'];
|
||||||
|
$instances = $matrixData['instances'];
|
||||||
|
$spotkania_cols = $matrixData['spotkania_cols'];
|
||||||
|
$bni_groups = $matrixData['bni_groups'];
|
||||||
|
$all_functions = $matrixData['all_functions'];
|
||||||
|
|
||||||
|
|
||||||
$status_colors = [
|
$status_colors = [
|
||||||
'completed' => '#28a745',
|
'none' => 'secondary',
|
||||||
'positive' => '#28a745',
|
'negative' => 'danger',
|
||||||
'in_progress' => '#fd7e14',
|
'in_progress' => 'warning',
|
||||||
'negative' => '#dc3545',
|
'positive' => 'success',
|
||||||
'error' => '#dc3545',
|
|
||||||
'none' => '#808080',
|
|
||||||
'not_started' => '#808080',
|
|
||||||
'inactive' => '#808080',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<?php include '_header.php'; ?>
|
<?php include '_header.php'; ?>
|
||||||
<?php include '_navbar.php'; ?>
|
<?php include '_navbar.php'; ?>
|
||||||
@ -45,7 +54,6 @@ $status_colors = [
|
|||||||
<?php include '_sidebar.php'; ?>
|
<?php include '_sidebar.php'; ?>
|
||||||
|
|
||||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||||
<div id="main-dashboard-view">
|
|
||||||
<?php if (isset($_SESSION['success_message'])): ?>
|
<?php if (isset($_SESSION['success_message'])): ?>
|
||||||
<div class="alert alert-success alert-dismissible fade show mt-3" role="alert">
|
<div class="alert alert-success alert-dismissible fade show mt-3" role="alert">
|
||||||
<?= $_SESSION['success_message']; ?>
|
<?= $_SESSION['success_message']; ?>
|
||||||
@ -85,82 +93,65 @@ $status_colors = [
|
|||||||
// Define process groups
|
// Define process groups
|
||||||
// Show all processes from the DB directly.
|
// Show all processes from the DB directly.
|
||||||
$inne_procesy_cols = $processes;
|
$inne_procesy_cols = $processes;
|
||||||
|
$inne_procesy_cols[] = ['id' => 'temp_szkolenia', 'name' => 'Szkolenia dla nowego członka'];
|
||||||
|
|
||||||
|
|
||||||
// --- Spotkania Columns ---
|
|
||||||
// Fetch upcoming meetings for each group
|
|
||||||
$today = date('Y-m-d H:i:s');
|
|
||||||
$stmt_meetings = $pdo->prepare("
|
|
||||||
SELECT bni_groups.id as group_id, bni_groups.name as group_name, MIN(calendar_events.start_datetime) as next_meeting_date
|
|
||||||
FROM bni_groups
|
|
||||||
LEFT JOIN calendar_event_groups ON bni_groups.id = calendar_event_groups.bni_group_id
|
|
||||||
LEFT JOIN calendar_events ON calendar_event_groups.calendar_event_id = calendar_events.id AND calendar_events.start_datetime >= :today
|
|
||||||
GROUP BY bni_groups.id
|
|
||||||
ORDER BY bni_groups.name
|
|
||||||
");
|
|
||||||
$stmt_meetings->execute(['today' => $today]);
|
|
||||||
$spotkania_cols = $stmt_meetings->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="groupFilter" class="form-label">Filter by Group</label>
|
<label class="form-label">Filter by Group:</label>
|
||||||
<select class="form-select" id="groupFilter">
|
<div class="btn-group" role="group" aria-label="Group Filter">
|
||||||
<option value="">All Groups</option>
|
<a href="index.php?<?= http_build_query(array_merge($_GET, ['group_id' => ''])) ?>" class="btn btn-outline-primary <?= !$groupId ? 'active-filter' : '' ?>">All Groups</a>
|
||||||
<?php foreach ($bni_groups as $group): ?>
|
<?php foreach ($bni_groups as $group): ?>
|
||||||
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
|
<a href="index.php?<?= http_build_query(array_merge($_GET, ['group_id' => $group['id']])) ?>" class="btn btn-outline-primary <?= ($groupId == $group['id']) ? 'active-filter' : '' ?>">
|
||||||
|
<?= htmlspecialchars($group['name']) ?>
|
||||||
|
</a>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<?php
|
|
||||||
// Find the meeting process ID from the already-fetched definitions
|
|
||||||
$meeting_process_id = 'null';
|
|
||||||
foreach ($matrix['definitions'] as $definition) {
|
|
||||||
if ($definition['name'] === 'Przygotowanie spotkania grupy') {
|
|
||||||
$meeting_process_id = $definition['id'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<table class="table table-bordered table-sm">
|
<table class="table table-bordered table-sm">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr class="text-center">
|
<tr class="text-center">
|
||||||
<th rowspan="2" class="align-middle"><input type="checkbox" id="selectAll"></th>
|
<th rowspan="2" class="align-middle"><input type="checkbox" id="selectAll"></th>
|
||||||
<th rowspan="2" class="align-middle">Person</th>
|
<th rowspan="2" class="align-middle">Person</th>
|
||||||
<?php if (!empty($spotkania_cols)): ?>
|
<?php if (!empty($spotkania_cols)): ?>
|
||||||
<th id="spotkania-header" colspan="<?= count($spotkania_cols) ?>">Spotkania</th>
|
<th colspan="<?= count($spotkania_cols) ?>">Spotkania</th>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (!empty($inne_procesy_cols)): ?>
|
<?php if (!empty($inne_procesy_cols)): ?>
|
||||||
<th colspan="<?= count($inne_procesy_cols) ?>">Inne procesy</th>
|
<th colspan="<?= count($inne_procesy_cols) ?>">Inne procesy</th>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="text-center" id="processes-header-row">
|
<tr class="text-center">
|
||||||
<?php foreach ($spotkania_cols as $index => $col): ?>
|
<?php foreach ($spotkania_cols as $col): ?>
|
||||||
<th class="process-header-clickable" data-process-id="<?= $meeting_process_id ?>" style="cursor: pointer;" data-group-id="<?= $col['group_id'] ?>" data-col-index="<?= $index ?>">
|
<th>
|
||||||
<?= htmlspecialchars($col['group_name']) ?><br>
|
<?= htmlspecialchars($col['group_name']) ?><br>
|
||||||
<small>
|
<small><?= $col['next_meeting_date'] ? date('d.m.Y', strtotime($col['next_meeting_date'])) : 'Brak' ?></small>
|
||||||
<?= $col['next_meeting_date'] ? date('d.m.Y', strtotime($col['next_meeting_date'])) : 'Brak' ?>
|
|
||||||
<?php if($col['next_meeting_date']): ?>
|
|
||||||
<i class="bi bi-arrow-right-short expand-meeting" style="cursor: pointer;" data-group-id="<?= $col['group_id'] ?>"></i>
|
|
||||||
<?php endif; ?>
|
|
||||||
</small>
|
|
||||||
</th>
|
</th>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php foreach ($inne_procesy_cols as $col): ?>
|
<?php foreach ($processes as $col): ?>
|
||||||
<th class="process-header-clickable" data-process-id="<?= $col['id'] ?>" style="cursor: pointer;"><?= htmlspecialchars($col['name']) ?></th>
|
<?php
|
||||||
|
$filterParams = $_GET;
|
||||||
|
$filterParams['active_process_id'] = $col['id'];
|
||||||
|
$link = 'index.php?' . http_build_query($filterParams);
|
||||||
|
$isActive = $activeProcessId == $col['id'];
|
||||||
|
?>
|
||||||
|
<th class="<?= $isActive ? 'active-filter' : '' ?>">
|
||||||
|
<a href="<?= $link ?>" class="text-decoration-none text-dark"><?= htmlspecialchars($col['name']) ?></a>
|
||||||
|
</th>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($people as $person): ?>
|
<?php foreach ($people as $person): ?>
|
||||||
<tr data-group-id="<?= $person['bni_group_id'] ?>">
|
<tr data-group-id="<?= $person['bni_group_id'] ?>">
|
||||||
<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="person_ids[]" 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['first_name'] . ' ' . $person['last_name']) ?></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['company_name']) ?></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'])): ?>
|
||||||
@ -185,15 +176,15 @@ $status_colors = [
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<?php // Spotkania Columns ?>
|
<?php // Spotkania Columns ?>
|
||||||
<?php foreach ($spotkania_cols as $index => $col): ?>
|
<?php foreach ($spotkania_cols as $col): ?>
|
||||||
<td class="text-center align-middle meeting-cell" data-group-id="<?= $col['group_id'] ?>" data-col-index="<?= $index ?>">
|
<td class="text-center align-middle">
|
||||||
<?php
|
<?php
|
||||||
// Placeholder Status: Logic for meeting attendance is not yet defined.
|
// Placeholder Status: Logic for meeting attendance is not yet defined.
|
||||||
// Display icon only if the person belongs to the group for that column.
|
// Display icon only if the person belongs to the group for that column.
|
||||||
if ($person['bni_group_id'] == $col['group_id']) {
|
if ($person['bni_group_id'] == $col['group_id']) {
|
||||||
$status = 'none'; // Default/placeholder status
|
$status = 'none'; // Default/placeholder status
|
||||||
$color = $status_colors[$status] ?? '#808080';
|
$color = $status_colors[$status];
|
||||||
echo "<span style='width: 20px; height: 20px; display: inline-block; border-radius: 50%; background-color: $color;' title='Status nieokreślony'></span>";
|
echo "<span class=\"badge rounded-circle bg-$color\" style=\"width: 20px; height: 20px; display: inline-block;\" title=\"Status nieokreślony\"></span>";
|
||||||
} else {
|
} else {
|
||||||
echo ''; // Empty cell if person is not in this group
|
echo ''; // Empty cell if person is not in this group
|
||||||
}
|
}
|
||||||
@ -204,67 +195,20 @@ $status_colors = [
|
|||||||
<?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;
|
||||||
$lastActivity = $instance && isset($instance['last_activity_at']) ? date('d/m/y', strtotime($instance['last_activity_at'])) : '';
|
$status = $instance ? $instance['computed_status'] : 'none';
|
||||||
|
$color = $status_colors[$status] ?? 'secondary';
|
||||||
// Correctly check eligibility using the WorkflowEngine
|
$lastActivity = $instance && $instance['last_activity_at'] ? date('d/m/y', strtotime($instance['last_activity_at'])) : '';
|
||||||
$eligibilityCheck = $workflowEngine->checkEligibility($person['id'], $process['id']);
|
|
||||||
$is_eligible = $eligibilityCheck['is_eligible'];
|
|
||||||
|
|
||||||
$is_active = $process['is_active'] ?? true;
|
|
||||||
$modal_target = ''; // Default to not clickable
|
|
||||||
$is_clickable = false;
|
|
||||||
|
|
||||||
if (!$is_active) {
|
|
||||||
$status = 'inactive';
|
|
||||||
$color = $status_colors['inactive'];
|
|
||||||
$title = 'Process inactive';
|
|
||||||
} elseif ($instance && isset($instance['id'])) { // Existing instance
|
|
||||||
$status = $instance['computed_status'];
|
|
||||||
$color = $status_colors[$status] ?? $status_colors['inactive'];
|
|
||||||
$title = !empty($instance['computed_reason']) ? $instance['computed_reason'] : ucfirst($status);
|
|
||||||
$modal_target = '#instanceModal';
|
|
||||||
$is_clickable = true;
|
|
||||||
} else { // No instance
|
|
||||||
if ($is_eligible) {
|
|
||||||
$status = 'not_started';
|
|
||||||
$color = $status_colors[$status];
|
|
||||||
$title = 'Not Started';
|
|
||||||
$modal_target = '#instanceModal';
|
|
||||||
$is_clickable = true;
|
|
||||||
} else {
|
|
||||||
$status = 'ineligible';
|
|
||||||
$color = '#e9ecef'; // A light gray color for the circle
|
|
||||||
$title = implode(' ', $eligibilityCheck['reasons']); // Use the reason from the engine
|
|
||||||
$modal_target = '#instanceModal'; // Still open the modal to show details
|
|
||||||
$is_clickable = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
?>
|
||||||
<td class="text-center align-middle"
|
<td class="text-center align-middle" data-bs-toggle="modal" data-bs-target="#instanceModal" data-person-id="<?= $person['id'] ?>" data-process-id="<?= $process['id'] ?>">
|
||||||
<?php if ($is_clickable): ?>
|
<span class="badge rounded-circle bg-<?= $color ?>" style="width: 20px; height: 20px; display: inline-block;" title="<?= ucfirst($status) ?>"> </span>
|
||||||
style="cursor: pointer;"
|
<small class="text-muted d-block"><?= $lastActivity ?></small>
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="<?= $modal_target ?>"
|
|
||||||
data-person-id="<?= $person['id'] ?>"
|
|
||||||
data-process-id="<?= $process['id'] ?>"
|
|
||||||
<?php else: ?>
|
|
||||||
style="cursor: not-allowed;"
|
|
||||||
<?php endif; ?>
|
|
||||||
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>
|
</td>
|
||||||
<?php
|
<?php endforeach; ?>
|
||||||
endforeach; ?>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div id="process-detail-view" style="display: none;">
|
|
||||||
<!-- This will be populated dynamically -->
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -279,17 +223,16 @@ $status_colors = [
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div id="editPersonError" class="alert alert-danger" style="display: none;"></div>
|
|
||||||
<input type="hidden" name="id" id="editPersonId">
|
<input type="hidden" name="id" id="editPersonId">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<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="first_name" required>
|
<input type="text" class="form-control" id="editFirstName" name="firstName" 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="last_name" required>
|
<input type="text" class="form-control" id="editLastName" name="lastName" 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>
|
||||||
@ -332,7 +275,7 @@ $status_colors = [
|
|||||||
</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="company_name">
|
<input type="text" class="form-control" id="editCompanyName" name="companyName">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
@ -388,8 +331,6 @@ $status_colors = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-3">
|
|
||||||
<div id="followUpSummaryContainer"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Zamknij</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Zamknij</button>
|
||||||
@ -410,17 +351,16 @@ $status_colors = [
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div id="createPersonError" class="alert alert-danger" style="display: none;"></div>
|
|
||||||
<form id="createPersonForm" action="_create_person.php" method="post" enctype="multipart/form-data">
|
<form id="createPersonForm" action="_create_person.php" method="post" enctype="multipart/form-data">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<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="first_name" required>
|
<input type="text" class="form-control" id="createFirstName" name="firstName" 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="last_name" required>
|
<input type="text" class="form-control" id="createLastName" name="lastName" 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>
|
||||||
@ -464,7 +404,7 @@ $status_colors = [
|
|||||||
</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="company_name">
|
<input type="text" class="form-control" id="createCompanyName" name="companyName">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
@ -565,440 +505,214 @@ $status_colors = [
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// --- Injected PHP Data ---
|
var instanceModal = document.getElementById('instanceModal');
|
||||||
const peopleData = <?= json_encode($people) ?>;
|
|
||||||
|
|
||||||
// --- STATE MANAGEMENT ---
|
|
||||||
const meetingsState = {};
|
|
||||||
|
|
||||||
// --- MODAL LOGIC ---
|
|
||||||
const instanceModal = document.getElementById('instanceModal');
|
|
||||||
let currentPersonId = null;
|
|
||||||
let currentProcessId = null;
|
|
||||||
|
|
||||||
if (instanceModal) {
|
|
||||||
// Event listener for when the modal is about to be shown
|
|
||||||
instanceModal.addEventListener('show.bs.modal', function (event) {
|
instanceModal.addEventListener('show.bs.modal', function (event) {
|
||||||
const button = event.relatedTarget;
|
var button = event.relatedTarget;
|
||||||
currentPersonId = button.getAttribute('data-person-id');
|
var personId = button.dataset.personId;
|
||||||
currentProcessId = button.getAttribute('data-process-id');
|
var processId = button.dataset.processId;
|
||||||
|
var modalBody = instanceModal.querySelector('.modal-body');
|
||||||
|
|
||||||
const modalBody = instanceModal.querySelector('.modal-body');
|
// Load content via AJAX
|
||||||
const modalTitle = instanceModal.querySelector('.modal-title');
|
fetch(`_get_instance_details.php?personId=${personId}&processId=${processId}`)
|
||||||
|
|
||||||
modalBody.innerHTML = '<div class="d-flex justify-content-center"><div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div></div>';
|
|
||||||
modalTitle.textContent = 'Ładowanie...';
|
|
||||||
|
|
||||||
// Fetch and display the initial modal content
|
|
||||||
fetchAndRenderModalContent(currentPersonId, currentProcessId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event listener for when the modal has been hidden
|
|
||||||
instanceModal.addEventListener('hidden.bs.modal', function () {
|
|
||||||
location.reload(); // Reload the main page to reflect any changes
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delegated event listener for all actions within the modal
|
|
||||||
instanceModal.addEventListener('click', function(event) {
|
|
||||||
const transitionBtn = event.target.closest('.apply-transition-btn');
|
|
||||||
if (transitionBtn) {
|
|
||||||
handleTransition(transitionBtn);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const noteBtn = event.target.closest('#addNoteBtn');
|
|
||||||
if (noteBtn) {
|
|
||||||
handleAddNote(noteBtn);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startBtn = event.target.closest('#startProcessBtn');
|
|
||||||
if (startBtn) {
|
|
||||||
handleStartProcess(startBtn);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
instanceModal.addEventListener('change', function(event) {
|
|
||||||
const checkbox = event.target.closest('.task-checkbox-modal');
|
|
||||||
if (checkbox) {
|
|
||||||
handleCheckboxChange(checkbox);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- HELPER FUNCTIONS FOR MODAL ---
|
|
||||||
|
|
||||||
function fetchAndRenderModalContent(personId, processId) {
|
|
||||||
const modalBody = instanceModal.querySelector('.modal-body');
|
|
||||||
const modalTitle = instanceModal.querySelector('.modal-title');
|
|
||||||
|
|
||||||
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;
|
||||||
const newTitleEl = modalBody.querySelector('#instance-modal-title');
|
});
|
||||||
if (newTitleEl) {
|
});
|
||||||
modalTitle.innerHTML = newTitleEl.innerHTML;
|
|
||||||
newTitleEl.remove();
|
// Bulk actions
|
||||||
} else {
|
const selectAll = document.getElementById('selectAll');
|
||||||
modalTitle.textContent = 'Szczegóły procesu';
|
const checkboxes = document.querySelectorAll('.person-checkbox');
|
||||||
|
const bulkActionsGroup = document.getElementById('bulk-actions-group');
|
||||||
|
|
||||||
|
function toggleBulkActions() {
|
||||||
|
const anyChecked = Array.from(checkboxes).some(c => c.checked);
|
||||||
|
bulkActionsGroup.style.display = anyChecked ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(error => {
|
selectAll.addEventListener('change', function () {
|
||||||
console.error('Error fetching instance details:', error);
|
checkboxes.forEach(c => c.checked = selectAll.checked);
|
||||||
modalBody.innerHTML = '<p class="text-danger">Wystąpił błąd podczas ładowania danych.</p>';
|
toggleBulkActions();
|
||||||
modalTitle.textContent = 'Błąd';
|
});
|
||||||
|
|
||||||
|
checkboxes.forEach(c => c.addEventListener('change', toggleBulkActions));
|
||||||
|
|
||||||
|
// Pass selected people to modals
|
||||||
|
function setupBulkModal(modalId, hiddenInputId) {
|
||||||
|
const modal = document.getElementById(modalId);
|
||||||
|
modal.addEventListener('show.bs.modal', function() {
|
||||||
|
const selectedIds = Array.from(checkboxes).filter(c => c.checked).map(c => c.value);
|
||||||
|
document.getElementById(hiddenInputId).value = JSON.stringify(selectedIds);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCheckboxChange(checkbox) {
|
setupBulkModal('bulkStatusModal', 'bulkStatusPersonIds');
|
||||||
const taskCode = checkbox.dataset.taskCode;
|
setupBulkModal('bulkEventModal', 'bulkEventPersonIds');
|
||||||
const isChecked = checkbox.checked;
|
setupBulkModal('bulkInitModal', 'bulkInitPersonIds');
|
||||||
const instanceId = checkbox.closest('.modal-body').querySelector('[data-instance-id]').dataset.instanceId;
|
|
||||||
|
|
||||||
fetch('_update_training_checklist_status.php', {
|
// Populate edit person modal
|
||||||
method: 'POST',
|
const editPersonModal = document.getElementById('editPersonModal');
|
||||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
if(editPersonModal) {
|
||||||
body: JSON.stringify({
|
editPersonModal.addEventListener('show.bs.modal', function (event) {
|
||||||
instance_id: instanceId,
|
const button = event.relatedTarget;
|
||||||
task_code: taskCode,
|
const personId = button.getAttribute('data-person-id');
|
||||||
is_checked: isChecked
|
var personName = button.getAttribute('data-person-name');
|
||||||
})
|
|
||||||
})
|
var deleteBtn = document.getElementById('deleteUserBtn');
|
||||||
.then(response => {
|
deleteBtn.dataset.personId = personId;
|
||||||
if (!response.ok) {
|
deleteBtn.dataset.personName = personName;
|
||||||
throw new Error('Network response was not ok');
|
|
||||||
}
|
fetch('_get_person_details.php?id=' + personId)
|
||||||
return response.json();
|
.then(response => response.json())
|
||||||
})
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (!data.success) {
|
document.getElementById('editPersonId').value = data.person.id;
|
||||||
console.error('Failed to update checklist status:', data.error);
|
document.getElementById('editFirstName').value = data.person.firstName;
|
||||||
checkbox.checked = !isChecked; // Revert on failure
|
document.getElementById('editLastName').value = data.person.lastName;
|
||||||
alert('Error updating status: ' + data.error);
|
document.getElementById('editEmail').value = data.person.email;
|
||||||
|
document.getElementById('editCompanyName').value = data.person.companyName;
|
||||||
|
document.getElementById('editPhone').value = data.person.phone;
|
||||||
|
document.getElementById('editRole').value = data.person.role;
|
||||||
|
document.getElementById('editBniGroup').value = data.person.bni_group_id || '';
|
||||||
|
|
||||||
|
document.getElementById('editNip').value = data.person.nip || '';
|
||||||
|
document.getElementById('editIndustry').value = data.person.industry || '';
|
||||||
|
document.getElementById('editCompanySize').value = data.person.company_size_revenue || '';
|
||||||
|
document.getElementById('editBusinessDescription').value = data.person.business_description || '';
|
||||||
|
|
||||||
|
document.getElementById('editCompanyLogoPath').textContent = data.person.company_logo_path || '';
|
||||||
|
document.getElementById('editPersonPhotoPath').textContent = data.person.person_photo_path || '';
|
||||||
|
document.getElementById('editGainsSheetPath').textContent = data.person.gains_sheet_path || '';
|
||||||
|
document.getElementById('editTopWantedPath').textContent = data.person.top_wanted_contacts_path || '';
|
||||||
|
document.getElementById('editTopOwnedPath').textContent = data.person.top_owned_contacts_path || '';
|
||||||
|
|
||||||
|
// Trigger change to show/hide group div and member-only fields
|
||||||
|
const editRoleSelect = document.getElementById('editRole');
|
||||||
|
editRoleSelect.dispatchEvent(new Event('change'));
|
||||||
|
|
||||||
|
const functionsSelect = document.getElementById('editRoles');
|
||||||
|
functionsSelect.innerHTML = ''; // Clear existing options
|
||||||
|
|
||||||
|
// Group functions by group_name
|
||||||
|
const groupedFunctions = data.all_functions.reduce((acc, func) => {
|
||||||
|
const groupName = func.group_name || 'Other';
|
||||||
|
if (!acc[groupName]) {
|
||||||
|
acc[groupName] = [];
|
||||||
}
|
}
|
||||||
// No reload on success, keep modal open
|
acc[groupName].push(func);
|
||||||
})
|
return acc;
|
||||||
.catch(error => {
|
}, {});
|
||||||
console.error('Network or server error during checklist update:', error);
|
|
||||||
checkbox.checked = !isChecked; // Revert on network error
|
// Populate select with optgroups
|
||||||
alert('A network error occurred. Please try again.');
|
for (const groupName in groupedFunctions) {
|
||||||
|
const optgroup = document.createElement('optgroup');
|
||||||
|
optgroup.label = groupName;
|
||||||
|
groupedFunctions[groupName].forEach(func => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = func.id;
|
||||||
|
option.textContent = func.name;
|
||||||
|
if (data.person_functions.map(String).includes(String(func.id))) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
optgroup.appendChild(option);
|
||||||
|
});
|
||||||
|
functionsSelect.appendChild(optgroup);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTransition(button) {
|
|
||||||
const instanceId = button.dataset.instanceId;
|
|
||||||
const transitionId = button.dataset.transitionId;
|
|
||||||
|
|
||||||
if (!confirm(`Czy na pewno chcesz wykonać akcję \"${button.textContent.trim()}\"?`)) {
|
// When the delete button in the edit modal is clicked, pass the data to the delete confirmation modal
|
||||||
return;
|
document.getElementById('deleteUserBtn').addEventListener('click', function() {
|
||||||
}
|
var personId = this.dataset.personId;
|
||||||
|
var personName = this.dataset.personName;
|
||||||
const form = document.getElementById('transition-form');
|
|
||||||
const formData = form ? new FormData(form) : new FormData();
|
var deletePersonModal = document.getElementById('deletePersonModal');
|
||||||
|
deletePersonModal.querySelector('#personNameToDelete').textContent = personName;
|
||||||
formData.append('instanceId', instanceId);
|
deletePersonModal.dataset.personId = personId;
|
||||||
formData.append('transitionId', transitionId);
|
});
|
||||||
|
|
||||||
// Add outcome_status based on transitionId for the new workflow
|
// Populate delete person modal
|
||||||
if (transitionId === 'log_wants_to_join') {
|
var deletePersonModal = document.getElementById('deletePersonModal');
|
||||||
formData.append('outcome_status', 'wants_to_join');
|
deletePersonModal.addEventListener('show.bs.modal', function (event) {
|
||||||
} else if (transitionId === 'log_declined') {
|
var button = event.relatedTarget;
|
||||||
formData.append('outcome_status', 'declined');
|
if (button.id !== 'deleteUserBtn') {
|
||||||
} else if (transitionId === 'log_no_answer') {
|
var personId = button.dataset.personId;
|
||||||
formData.append('outcome_status', 'no_answer');
|
var personName = button.dataset.personName;
|
||||||
} else if (transitionId === 'log_call_later') {
|
document.getElementById('personNameToDelete').textContent = personName;
|
||||||
formData.append('outcome_status', 'call_later');
|
deletePersonModal.dataset.personId = personId;
|
||||||
}
|
|
||||||
|
|
||||||
submitRequestAndReloadModal('_apply_transition.php', formData);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAddNote(button) {
|
|
||||||
const instanceId = button.dataset.instanceId;
|
|
||||||
const message = document.getElementById('noteMessage').value;
|
|
||||||
|
|
||||||
if (!message.trim()) {
|
|
||||||
alert('Proszę wpisać treść notatki.');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('confirmDeleteBtn').addEventListener('click', function() {
|
||||||
|
let personIdToDelete = deletePersonModal.dataset.personId;
|
||||||
|
if (personIdToDelete) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('instanceId', instanceId);
|
formData.append('person_id', personIdToDelete);
|
||||||
formData.append('transitionId', 'note'); // Special transitionId
|
|
||||||
formData.append('payload[message]', message);
|
|
||||||
submitRequestAndReloadModal('_apply_transition.php', formData);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleStartProcess(button) {
|
fetch('_delete_person.php', {
|
||||||
const personId = button.dataset.personId;
|
|
||||||
const processId = button.dataset.processId;
|
|
||||||
|
|
||||||
if (!personId || !processId) {
|
|
||||||
alert('Missing data for starting process. Please close the modal and try again.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('person_id', personId);
|
|
||||||
formData.append('process_id', processId);
|
|
||||||
|
|
||||||
submitRequestAndReloadModal('_init_single_instance.php', formData);
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitRequestAndReloadModal(url, formData) {
|
|
||||||
const modalBody = instanceModal.querySelector('.modal-body');
|
|
||||||
showLoading(modalBody);
|
|
||||||
|
|
||||||
fetch(url, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
})
|
})
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
// If response is not OK, it's an error. Clone the response to read it twice.
|
|
||||||
const clone = response.clone();
|
|
||||||
return response.json()
|
|
||||||
.then(json => {
|
|
||||||
// We have a JSON error body, throw a custom error with its details
|
|
||||||
const error = new Error(json.error?.message || 'An unkown error occurred.');
|
|
||||||
error.correlation_id = json.correlation_id;
|
|
||||||
error.response = response; // Attach full response
|
|
||||||
throw error;
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// If JSON parsing fails, fall back to the text body
|
|
||||||
return clone.text().then(text => {
|
|
||||||
const error = new Error(text || 'Network response was not ok and could not parse error body.');
|
|
||||||
error.response = response;
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return response.json(); // On success, just parse the JSON
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
// Reload modal content for the same person/process after successful submission
|
|
||||||
fetchAndRenderModalContent(currentPersonId, currentProcessId);
|
|
||||||
} else {
|
|
||||||
// Handle cases where the server returns 200 OK but with success: false
|
|
||||||
const error = new Error(data.message || 'An unknown error occurred.');
|
|
||||||
if (data.correlation_id) {
|
|
||||||
error.correlation_id = data.correlation_id;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error submitting request:', error);
|
|
||||||
|
|
||||||
let errorMessage = `<div class="alert alert-danger">`;
|
|
||||||
errorMessage += `<strong>Error:</strong> ${error.message}`;
|
|
||||||
if (error.correlation_id) {
|
|
||||||
errorMessage += `<br><small class="text-muted">Correlation ID: ${error.correlation_id}</small>`;
|
|
||||||
}
|
|
||||||
errorMessage += `</div>`;
|
|
||||||
modalBody.innerHTML = errorMessage;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLoading(element) {
|
|
||||||
element.innerHTML = '<div class="d-flex justify-content-center align-items-center" style="min-height: 200px;"><div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div></div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- GROUP FILTER LOGIC ---
|
|
||||||
const groupFilter = document.getElementById('groupFilter');
|
|
||||||
if (groupFilter) {
|
|
||||||
groupFilter.addEventListener('change', function () {
|
|
||||||
const selectedGroupId = this.value;
|
|
||||||
document.querySelectorAll('tbody tr').forEach(row => {
|
|
||||||
const rowGroupId = row.getAttribute('data-group-id');
|
|
||||||
row.style.display = (selectedGroupId === '' || rowGroupId === selectedGroupId) ? '' : 'none';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- EXPAND MEETINGS LOGIC ---
|
|
||||||
const headerRow = document.getElementById('processes-header-row');
|
|
||||||
if (headerRow) {
|
|
||||||
headerRow.addEventListener('click', function (event) {
|
|
||||||
const expandBtn = event.target.closest('.expand-meeting');
|
|
||||||
if (!expandBtn) return;
|
|
||||||
|
|
||||||
event.stopPropagation(); // Prevent the click from bubbling up to the process-header-clickable listener
|
|
||||||
|
|
||||||
const groupId = expandBtn.dataset.groupId;
|
|
||||||
expandBtn.style.display = 'none';
|
|
||||||
|
|
||||||
if (!meetingsState[groupId]) {
|
|
||||||
meetingsState[groupId] = { offset: 1 };
|
|
||||||
}
|
|
||||||
|
|
||||||
const limit = 2;
|
|
||||||
const offset = meetingsState[groupId].offset;
|
|
||||||
meetingsState[groupId].offset += limit;
|
|
||||||
|
|
||||||
const fetchUrl = `_get_future_meetings.php?bni_group_id=${groupId}&limit=${limit}&offset=${offset}`;
|
|
||||||
|
|
||||||
fetch(fetchUrl)
|
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.error) {
|
const modal = bootstrap.Modal.getInstance(deletePersonModal);
|
||||||
console.error('BŁĄD Z SERWERA:', data.error);
|
modal.hide();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
if (data.success) {
|
||||||
let lastThForGroup = findLastElementForGroup(headerRow, 'th', groupId);
|
window.location.reload();
|
||||||
if (!lastThForGroup) {
|
} else {
|
||||||
console.error('KRYTYCZNY BŁĄD: Nie można znaleźć nagłówka startowego (TH) dla grupy.');
|
let errorAlert = document.querySelector('.alert-danger');
|
||||||
return;
|
let errorContainer = errorAlert.parentElement;
|
||||||
}
|
if (!errorAlert) {
|
||||||
|
errorContainer = document.querySelector('main'); // fallback
|
||||||
data.forEach((meeting) => {
|
const alertHTML = `
|
||||||
const newTh = document.createElement('th');
|
<div class="alert alert-danger alert-dismissible fade show mt-3" role="alert">
|
||||||
newTh.dataset.groupId = groupId;
|
${data.error}
|
||||||
newTh.className = 'text-center';
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>`;
|
||||||
const meetingDate = new Date(meeting.start_datetime);
|
errorContainer.insertAdjacentHTML('afterbegin', alertHTML);
|
||||||
const formattedDate = meetingDate.toLocaleDateString('pl-PL', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
} else {
|
||||||
|
errorAlert.innerHTML = `${data.error} <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>`;
|
||||||
const isLastInBatch = false;
|
errorAlert.classList.remove('d-none');
|
||||||
const isLastInGroup = false;
|
|
||||||
|
|
||||||
let iconHtml = '';
|
|
||||||
if (isLastInBatch && !isLastInGroup) {
|
|
||||||
iconHtml = ` <i class="bi bi-arrow-right-short expand-meeting" data-group-id="${groupId}" style="cursor: pointer;"></i>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
newTh.innerHTML = `<span class=\"text-muted\">${htmlspecialchars(meeting.group_name || '')}</span><br><small>${formattedDate}${iconHtml}</small>`;
|
|
||||||
|
|
||||||
lastThForGroup.after(newTh);
|
|
||||||
lastThForGroup = newTh;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('tbody tr').forEach((personRow) => {
|
|
||||||
let lastTdForGroup = findLastElementForGroup(personRow, 'td.meeting-cell', groupId);
|
|
||||||
|
|
||||||
if (!lastTdForGroup) {
|
|
||||||
// Fallback for rows that might not have the initial meeting cell
|
|
||||||
const allHeaders = Array.from(headerRow.children);
|
|
||||||
const initialTh = findLastElementForGroup(headerRow, 'th', groupId, true);
|
|
||||||
const anchorIndex = allHeaders.indexOf(initialTh);
|
|
||||||
lastTdForGroup = personRow.children[anchorIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lastTdForGroup) {
|
|
||||||
return; // Skip row if anchor is still not found
|
|
||||||
}
|
|
||||||
|
|
||||||
data.forEach(() => {
|
|
||||||
const newTd = document.createElement('td');
|
|
||||||
newTd.dataset.groupId = groupId;
|
|
||||||
newTd.className = 'text-center align-middle meeting-cell';
|
|
||||||
|
|
||||||
if (personRow.dataset.groupId === groupId) {
|
|
||||||
newTd.innerHTML = `<span class="badge rounded-circle bg-secondary" style="width: 20px; height: 20px; display: inline-block;" title="Status nieokreślony"></span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastTdForGroup.after(newTd);
|
|
||||||
lastTdForGroup = newTd;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const spotkaniaHeader = document.getElementById('spotkania-header');
|
|
||||||
if (spotkaniaHeader) {
|
|
||||||
spotkaniaHeader.colSpan = (spotkaniaHeader.colSpan || 1) + data.length;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('KRYTYCZNY BŁĄD SIECI LUB PARSOWANIA:', error);
|
console.error('Error:', error);
|
||||||
});
|
const modal = bootstrap.Modal.getInstance(deletePersonModal);
|
||||||
|
modal.hide();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function findLastElementForGroup(parent, selector, groupId, findFirst = false) {
|
|
||||||
const elements = parent.querySelectorAll(`${selector}[data-group-id="${groupId}"]`);
|
|
||||||
if (elements.length === 0) return null;
|
|
||||||
return findFirst ? elements[0] : elements[elements.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
function htmlspecialchars(str) {
|
|
||||||
if (typeof str !== 'string') return '';
|
|
||||||
return str.replace(/[&<>"]/g, match => ({
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"'
|
|
||||||
}[match]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- PROCESS BULK VIEW LOGIC ---
|
|
||||||
|
|
||||||
const processDetailView = document.getElementById('process-detail-view');
|
|
||||||
|
|
||||||
function renderProcessDetailView(data) {
|
|
||||||
const { process, steps, instances } = data;
|
|
||||||
|
|
||||||
let tableHtml = `<div class="d-flex justify-content-between align-items-center pt-3 pb-2 mb-3 border-bottom">
|
|
||||||
<h1 class="h2">Process: ${htmlspecialchars(process.name)}</h1>
|
|
||||||
<button class="btn btn-secondary" id="back-to-dashboard">Back to Dashboard</button>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
tableHtml += '<div class="table-responsive"><table class="table table-bordered table-sm">';
|
|
||||||
|
|
||||||
tableHtml += '<thead class="table-light"><tr><th>Person</th>';
|
|
||||||
steps.forEach(step => {
|
|
||||||
tableHtml += `<th>${htmlspecialchars(step.name)}</th>`;
|
|
||||||
});
|
|
||||||
tableHtml += '</tr></thead>';
|
|
||||||
|
|
||||||
tableHtml += '<tbody>';
|
|
||||||
instances.forEach(instance => {
|
|
||||||
const person = peopleData.find(p => p.id == instance.person_id);
|
|
||||||
if (!person) return;
|
|
||||||
|
|
||||||
tableHtml += `<tr><td class="person-cell">
|
|
||||||
<div class="person-main">
|
|
||||||
<div class="person-name">${htmlspecialchars(person.first_name + ' ' + person.last_name)}</div>
|
|
||||||
</div>
|
|
||||||
</td>`;
|
|
||||||
|
|
||||||
steps.forEach(step => {
|
|
||||||
const stepState = instance.steps.find(s => s.step_id == step.id);
|
|
||||||
const status = stepState ? stepState.status : 'pending';
|
|
||||||
const statusColors = {
|
|
||||||
pending: 'secondary',
|
|
||||||
in_progress: 'warning',
|
|
||||||
completed: 'success',
|
|
||||||
skipped: 'light',
|
|
||||||
failed: 'danger'
|
|
||||||
};
|
|
||||||
const color = statusColors[status] || 'secondary';
|
|
||||||
|
|
||||||
tableHtml += `<td class="text-center align-middle">
|
|
||||||
<span class="badge rounded-circle bg-${color}" style="width: 20px; height: 20px; display: inline-block;" title="${status}"> </span>
|
|
||||||
</td>`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tableHtml += '</tr>';
|
// Handle role change for group visibility
|
||||||
});
|
const createRoleSelect = document.getElementById('createRole');
|
||||||
tableHtml += '</tbody></table></div>';
|
const createGroupDiv = document.getElementById('create-group-selection-div');
|
||||||
|
const createMemberOnlyFields = document.querySelector('#createPersonModal .member-only-fields');
|
||||||
|
|
||||||
processDetailView.innerHTML = tableHtml;
|
createRoleSelect.addEventListener('change', function() {
|
||||||
|
const isMember = this.value === 'member';
|
||||||
document.getElementById('back-to-dashboard').addEventListener('click', () => {
|
createGroupDiv.style.display = isMember ? 'block' : 'none';
|
||||||
processDetailView.style.display = 'none';
|
createMemberOnlyFields.style.display = isMember ? 'block' : 'none';
|
||||||
mainDashboardView.style.display = 'block';
|
|
||||||
processDetailView.innerHTML = '';
|
|
||||||
});
|
});
|
||||||
}
|
// Initial check
|
||||||
|
const isMemberCreate = createRoleSelect.value === 'member';
|
||||||
|
createGroupDiv.style.display = isMemberCreate ? 'block' : 'none';
|
||||||
|
createMemberOnlyFields.style.display = isMemberCreate ? 'block' : 'none';
|
||||||
|
|
||||||
|
|
||||||
|
const editRoleSelect = document.getElementById('editRole');
|
||||||
|
const editGroupDiv = document.getElementById('edit-group-selection-div');
|
||||||
|
const editMemberOnlyFields = document.querySelector('#editPersonModal .member-only-fields');
|
||||||
|
|
||||||
|
editRoleSelect.addEventListener('change', function() {
|
||||||
|
const isMember = this.value === 'member';
|
||||||
|
editGroupDiv.style.display = isMember ? 'block' : 'none';
|
||||||
|
editMemberOnlyFields.style.display = isMember ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -1013,7 +727,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form action="_bulk_update_status.php" method="post">
|
<form action="_bulk_update_status.php" method="post">
|
||||||
<input type="hidden" name="personIds" id="bulkStatusPersonIds">
|
<input type="hidden" name="person_ids" id="bulkStatusPersonIds">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Process</label>
|
<label class="form-label">Process</label>
|
||||||
<select name="process_id" class="form-select" required>
|
<select name="process_id" class="form-select" required>
|
||||||
@ -1048,7 +762,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form action="_bulk_add_event.php" method="post">
|
<form action="_bulk_add_event.php" method="post">
|
||||||
<input type="hidden" name="personIds" id="bulkEventPersonIds">
|
<input type="hidden" name="person_ids" id="bulkEventPersonIds">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Process</label>
|
<label class="form-label">Process</label>
|
||||||
<select name="process_id" class="form-select" required>
|
<select name="process_id" class="form-select" required>
|
||||||
@ -1078,7 +792,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form action="_bulk_init_instances.php" method="post">
|
<form action="_bulk_init_instances.php" method="post">
|
||||||
<input type="hidden" name="personIds" id="bulkInitPersonIds">
|
<input type="hidden" name="person_ids" id="bulkInitPersonIds">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Process</label> <select name="process_id" class="form-select" required>
|
<label class="form-label">Process</label> <select name="process_id" class="form-select" required>
|
||||||
<?php foreach($processes as $process): ?>
|
<?php foreach($processes as $process): ?>
|
||||||
|
|||||||
109
instance_details.html
Normal file
109
instance_details.html
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
|
||||||
|
<!-- Title for the modal, to be grabbed by JS -->
|
||||||
|
<div id="instance-modal-title" class="d-none">
|
||||||
|
Staszek Ptaszek - Follow-up</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="process-steps-container">
|
||||||
|
<h5>Kroki procesu</h5>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center list-group-item-primary">
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-arrow-right-circle-fill text-primary me-2"></i> <strong>Follow-up Call</strong>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center text-muted">
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-circle me-2"></i> <strong></strong>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center text-muted">
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-circle me-2"></i> <strong>Call Scheduled</strong>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center text-muted">
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-circle me-2"></i> <strong>No Answer Logged</strong>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center text-muted">
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-circle me-2"></i> <strong>Wants to Join</strong>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center text-muted">
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-circle me-2"></i> <strong>Declined</strong>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center text-muted">
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-circle me-2"></i> <strong>Process Ended</strong>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center text-muted">
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-circle me-2"></i> <strong>Process Terminated</strong>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h5>Available Actions</h5>
|
||||||
|
<form id="transition-form">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="call_date" class="form-label">Call Date</label>
|
||||||
|
<input type="datetime-local" id="call_date" name="call_date" class="form-control" value="2026-01-11T07:00">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="outcome_status" class="form-label">Call Outcome</label>
|
||||||
|
<select id="outcome_status" name="outcome_status" class="form-select">
|
||||||
|
<option value="">-- Select Outcome --</option>
|
||||||
|
<option value="no_answer">No Answer</option>
|
||||||
|
<option value="wants_to_join">Wants to Join</option>
|
||||||
|
<option value="declined">Declined</option>
|
||||||
|
<option value="call_later">Call Later</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="note" class="form-label">Notes</label>
|
||||||
|
<textarea id="note" name="note" class="form-control"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="next_contact_date" class="form-label">Next Contact Date</label>
|
||||||
|
<input type="datetime-local" id="next_contact_date" name="next_contact_date" class="form-control" value="">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<button class="btn btn-sm btn-primary apply-transition-btn"
|
||||||
|
data-instance-id="21"
|
||||||
|
data-transition-id="submit_outcome">
|
||||||
|
Log Call Attempt </button>
|
||||||
|
<button class="btn btn-sm btn-primary apply-transition-btn"
|
||||||
|
data-instance-id="21"
|
||||||
|
data-transition-id="terminate_from_awaiting_call">
|
||||||
|
Zakończ proces </button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="add-note-container">
|
||||||
|
<h5>Dodaj notatkę</h5>
|
||||||
|
<div class="mb-3">
|
||||||
|
<textarea id="noteMessage" class="form-control" rows="2" placeholder="Wpisz treść notatki..."></textarea>
|
||||||
|
</div>
|
||||||
|
<button id="addNoteBtn" class="btn btn-secondary" data-instance-id="21">Dodaj notatkę</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="history-container">
|
||||||
|
<h5>Historia</h5>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>System</strong>
|
||||||
|
<p class="mb-1 text-muted fst-italic">Process started.</p> <small class="text-muted">By Admin User on 11.01.2026, 07:00</small>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
131
new_definition.json
Normal file
131
new_definition.json
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"start_node_id": "awaiting_call",
|
||||||
|
"eligibility_rules": [
|
||||||
|
{
|
||||||
|
"type": "person_property_equals",
|
||||||
|
"params": {
|
||||||
|
"property": "role",
|
||||||
|
"value": "guest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nodes": {
|
||||||
|
"awaiting_call": {
|
||||||
|
"ui_hints": {
|
||||||
|
"title": "Follow-up Call",
|
||||||
|
"status": "active",
|
||||||
|
"reason": "Ready to log a new call attempt with the guest.",
|
||||||
|
"next_step": "Fill out the form below to log the details of your call.",
|
||||||
|
"form_schema": [
|
||||||
|
{ "name": "call_date", "label": "Call Date", "type": "datetime-local", "default": "now", "required": true },
|
||||||
|
{
|
||||||
|
"name": "outcome_status",
|
||||||
|
"label": "Call Outcome",
|
||||||
|
"type": "select",
|
||||||
|
"required": true,
|
||||||
|
"options": [
|
||||||
|
{ "value": "", "label": "-- Select Outcome --" },
|
||||||
|
{ "value": "no_answer", "label": "No Answer" },
|
||||||
|
{ "value": "wants_to_join", "label": "Wants to Join" },
|
||||||
|
{ "value": "declined", "label": "Declined" },
|
||||||
|
{ "value": "call_later", "label": "Call Later" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "name": "note", "label": "Notes", "type": "textarea" },
|
||||||
|
{ "name": "next_contact_date", "label": "Next Contact Date", "type": "datetime-local", "condition": { "field": "outcome_status", "value": "call_later" }, "required": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outcome_router": { "ui_hints": { "status": "processing" } },
|
||||||
|
"waiting_for_next_contact": {
|
||||||
|
"ui_hints": {
|
||||||
|
"title": "Call Scheduled",
|
||||||
|
"status": "paused",
|
||||||
|
"reason": "A follow-up call is scheduled for a future date.",
|
||||||
|
"next_step": "Click 'Log Next Call Attempt' to log the call now or on the scheduled date."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"decide_after_no_answer": {
|
||||||
|
"ui_hints": {
|
||||||
|
"title": "No Answer Logged",
|
||||||
|
"status": "paused",
|
||||||
|
"reason": "The guest did not answer the last call.",
|
||||||
|
"next_step": "Decide whether to try again or end the follow-up process."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"end_positive": { "ui_hints": { "title": "Wants to Join", "status": "completed", "reason": "Guest wants to join. New member process started.", "next_step": "" } },
|
||||||
|
"end_negative_declined": { "ui_hints": { "title": "Declined", "status": "terminated", "reason": "Guest declined to join.", "next_step": "" } },
|
||||||
|
"end_negative_no_answer": { "ui_hints": { "title": "Process Ended", "status": "terminated", "reason": "Process ended after no answer.", "next_step": "" } },
|
||||||
|
"end_negative_terminated": { "ui_hints": { "title": "Process Terminated", "status": "terminated", "reason": "Process manually terminated by user.", "next_step": "" } }
|
||||||
|
},
|
||||||
|
"transitions": [
|
||||||
|
{
|
||||||
|
"id": "submit_outcome",
|
||||||
|
"from": "awaiting_call",
|
||||||
|
"to": "outcome_router",
|
||||||
|
"name": "Log Call Attempt",
|
||||||
|
"actions": [
|
||||||
|
{ "type": "set_data", "params": { "keys": ["call_date", "outcome_status", "note", "next_contact_date"] } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "route_wants_to_join",
|
||||||
|
"from": "outcome_router",
|
||||||
|
"to": "end_positive",
|
||||||
|
"name": "Route to Positive End",
|
||||||
|
"condition": { "field": "outcome_status", "value": "wants_to_join" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "start_process", "process_code": "wprowadzenie-nowego-cz-onka" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "route_declined",
|
||||||
|
"from": "outcome_router",
|
||||||
|
"to": "end_negative_declined",
|
||||||
|
"name": "Route to Declined",
|
||||||
|
"condition": { "field": "outcome_status", "value": "declined" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "route_no_answer",
|
||||||
|
"from": "outcome_router",
|
||||||
|
"to": "decide_after_no_answer",
|
||||||
|
"name": "Route to No Answer",
|
||||||
|
"condition": { "field": "outcome_status", "value": "no_answer" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "route_call_later",
|
||||||
|
"from": "outcome_router",
|
||||||
|
"to": "waiting_for_next_contact",
|
||||||
|
"name": "Route to Call Later",
|
||||||
|
"condition": { "field": "outcome_status", "value": "call_later" }
|
||||||
|
},
|
||||||
|
{ "id": "continue_attempts", "from": "decide_after_no_answer", "to": "awaiting_call", "name": "Try Again" },
|
||||||
|
{ "id": "end_attempts", "from": "decide_after_no_answer", "to": "end_negative_no_answer", "name": "End Process" },
|
||||||
|
{ "id": "log_next_attempt", "from": "waiting_for_next_contact", "to": "awaiting_call", "name": "Log Next Call Attempt" },
|
||||||
|
{
|
||||||
|
"id": "terminate_from_awaiting_call",
|
||||||
|
"from": "awaiting_call",
|
||||||
|
"to": "end_negative_terminated",
|
||||||
|
"name": "Zakończ proces",
|
||||||
|
"actions": [ { "type": "set_data", "params": { "keys": ["termination_note"] } } ],
|
||||||
|
"form_schema": [ { "name": "termination_note", "label": "Reason for Termination (optional)", "type": "textarea" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "terminate_from_decide",
|
||||||
|
"from": "decide_after_no_answer",
|
||||||
|
"to": "end_negative_terminated",
|
||||||
|
"name": "Zakończ proces",
|
||||||
|
"actions": [ { "type": "set_data", "params": { "keys": ["termination_note"] } } ],
|
||||||
|
"form_schema": [ { "name": "termination_note", "label": "Reason for Termination (optional)", "type": "textarea" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "terminate_from_waiting",
|
||||||
|
"from": "waiting_for_next_contact",
|
||||||
|
"to": "end_negative_terminated",
|
||||||
|
"name": "Zakończ proces",
|
||||||
|
"actions": [ { "type": "set_data", "params": { "keys": ["termination_note"] } } ],
|
||||||
|
"form_schema": [ { "name": "termination_note", "label": "Reason for Termination (optional)", "type": "textarea" } ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@ require_once 'WorkflowEngine.php';
|
|||||||
$workflowEngine = new WorkflowEngine();
|
$workflowEngine = new WorkflowEngine();
|
||||||
// TODO: Create a method in WorkflowEngine to get all process definitions
|
// TODO: Create a method in WorkflowEngine to get all process definitions
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
$stmt = $pdo->query("SELECT * FROM process_definitions ORDER BY name");
|
$stmt = $pdo->query("SELECT * FROM process_definitions WHERE is_active = 1 ORDER BY sort_order, name");
|
||||||
$processes = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$processes = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|||||||
165
update_follow_up_process.php
Normal file
165
update_follow_up_process.php
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
|
||||||
|
function update_process_definition()
|
||||||
|
{
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
$json_definition = <<<'EOT'
|
||||||
|
{
|
||||||
|
"start_node_id": "awaiting_call",
|
||||||
|
"eligibility_rules": [
|
||||||
|
{
|
||||||
|
"type": "person_property_equals",
|
||||||
|
"params": {
|
||||||
|
"property": "role",
|
||||||
|
"value": "guest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nodes": {
|
||||||
|
"awaiting_call": {
|
||||||
|
"ui_hints": {
|
||||||
|
"title": "Follow-up Call",
|
||||||
|
"status": "active",
|
||||||
|
"reason": "Awaiting follow-up call with the guest.",
|
||||||
|
"next_step": "Log the outcome of the call.",
|
||||||
|
"form_schema": [
|
||||||
|
{ "name": "call_date", "label": "Call Date", "type": "datetime-local", "default": "now", "required": true },
|
||||||
|
{
|
||||||
|
"name": "outcome_status",
|
||||||
|
"label": "Call Outcome",
|
||||||
|
"type": "select",
|
||||||
|
"required": true,
|
||||||
|
"options": [
|
||||||
|
{ "value": "", "label": "-- Select Outcome --" },
|
||||||
|
{ "value": "no_answer", "label": "No Answer" },
|
||||||
|
{ "value": "wants_to_join", "label": "Wants to Join" },
|
||||||
|
{ "value": "declined", "label": "Declined" },
|
||||||
|
{ "value": "call_later", "label": "Call Later" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "name": "note", "label": "Notes", "type": "textarea" },
|
||||||
|
{ "name": "next_contact_date", "label": "Next Contact Date", "type": "datetime-local", "condition": { "field": "outcome_status", "value": "call_later" }, "required": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outcome_router": { "ui_hints": { "status": "processing" } },
|
||||||
|
"waiting_for_next_contact": {
|
||||||
|
"ui_hints": {
|
||||||
|
"title": "Waiting for Scheduled Call",
|
||||||
|
"status": "paused",
|
||||||
|
"reason": "Waiting until the scheduled date for the next call.",
|
||||||
|
"next_step": "Resume contact on or after the scheduled date."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"decide_after_no_answer": {
|
||||||
|
"ui_hints": {
|
||||||
|
"title": "No Answer",
|
||||||
|
"status": "paused",
|
||||||
|
"reason": "The guest did not answer the call.",
|
||||||
|
"next_step": "Decide whether to try again or end the process."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"end_positive": { "ui_hints": { "title": "Wants to Join", "status": "completed", "reason": "Guest wants to join. New member process started.", "next_step": "" } },
|
||||||
|
"end_negative_declined": { "ui_hints": { "title": "Declined", "status": "terminated", "reason": "Guest declined to join.", "next_step": "" } },
|
||||||
|
"end_negative_no_answer": { "ui_hints": { "title": "Process Ended", "status": "terminated", "reason": "Process ended after no answer.", "next_step": "" } },
|
||||||
|
"end_negative_terminated": { "ui_hints": { "title": "Process Terminated", "status": "terminated", "reason": "Process manually terminated by user.", "next_step": "" } }
|
||||||
|
},
|
||||||
|
"transitions": [
|
||||||
|
{
|
||||||
|
"id": "submit_outcome",
|
||||||
|
"from": "awaiting_call",
|
||||||
|
"to": "outcome_router",
|
||||||
|
"name": "Submit Outcome",
|
||||||
|
"actions": [
|
||||||
|
{ "type": "set_data", "params": { "keys": ["call_date", "outcome_status", "note", "next_contact_date"] } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "route_wants_to_join",
|
||||||
|
"from": "outcome_router",
|
||||||
|
"to": "end_positive",
|
||||||
|
"name": "Route to Positive End",
|
||||||
|
"condition": { "field": "outcome_status", "value": "wants_to_join" },
|
||||||
|
"actions": [
|
||||||
|
{ "type": "start_process", "process_code": "obsluga-przyjecia-nowego-czlonka" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "route_declined",
|
||||||
|
"from": "outcome_router",
|
||||||
|
"to": "end_negative_declined",
|
||||||
|
"name": "Route to Declined",
|
||||||
|
"condition": { "field": "outcome_status", "value": "declined" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "route_no_answer",
|
||||||
|
"from": "outcome_router",
|
||||||
|
"to": "decide_after_no_answer",
|
||||||
|
"name": "Route to No Answer",
|
||||||
|
"condition": { "field": "outcome_status", "value": "no_answer" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "route_call_later",
|
||||||
|
"from": "outcome_router",
|
||||||
|
"to": "waiting_for_next_contact",
|
||||||
|
"name": "Route to Call Later",
|
||||||
|
"condition": { "field": "outcome_status", "value": "call_later" }
|
||||||
|
},
|
||||||
|
{ "id": "continue_attempts", "from": "decide_after_no_answer", "to": "awaiting_call", "name": "Try Again" },
|
||||||
|
{ "id": "end_attempts", "from": "decide_after_no_answer", "to": "end_negative_no_answer", "name": "End Process" },
|
||||||
|
{ "id": "resume_contact", "from": "waiting_for_next_contact", "to": "awaiting_call", "name": "Resume / Attempt Call" },
|
||||||
|
{
|
||||||
|
"id": "terminate_from_awaiting_call",
|
||||||
|
"from": "awaiting_call",
|
||||||
|
"to": "end_negative_terminated",
|
||||||
|
"name": "Zakończ proces",
|
||||||
|
"actions": [ { "type": "set_data", "params": { "keys": ["termination_note"] } } ],
|
||||||
|
"form_schema": [ { "name": "termination_note", "label": "Reason for Termination (optional)", "type": "textarea" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "terminate_from_decide",
|
||||||
|
"from": "decide_after_no_answer",
|
||||||
|
"to": "end_negative_terminated",
|
||||||
|
"name": "Zakończ proces",
|
||||||
|
"actions": [ { "type": "set_data", "params": { "keys": ["termination_note"] } } ],
|
||||||
|
"form_schema": [ { "name": "termination_note", "label": "Reason for Termination (optional)", "type": "textarea" } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "terminate_from_waiting",
|
||||||
|
"from": "waiting_for_next_contact",
|
||||||
|
"to": "end_negative_terminated",
|
||||||
|
"name": "Zakończ proces",
|
||||||
|
"actions": [ { "type": "set_data", "params": { "keys": ["termination_note"] } } ],
|
||||||
|
"form_schema": [ { "name": "termination_note", "label": "Reason for Termination (optional)", "type": "textarea" } ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOT;
|
||||||
|
|
||||||
|
// Update process_definition_id=4
|
||||||
|
$stmt = $pdo->prepare("UPDATE process_definitions SET definition_json = :json, start_node_id = 'awaiting_call' WHERE id = 4");
|
||||||
|
$stmt->execute([
|
||||||
|
':json' => $json_definition,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo "Updated process_definition_id=4 with the new follow-up workflow.\n";
|
||||||
|
|
||||||
|
// Deactivate process_definition_id=3
|
||||||
|
$stmt = $pdo->prepare("UPDATE process_definitions SET is_active = 0 WHERE id = 3");
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
echo "Deactivated process_definition_id=3 ('guest_handling').\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct execution guard
|
||||||
|
if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"]))
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
update_process_definition();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Failed to update process definitions: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
11
verify_fix.sh
Executable file
11
verify_fix.sh
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Login and save cookie
|
||||||
|
curl -s -L -c cookie.txt -X POST -F 'email=admin@example.com' -F 'password=password' http://localhost/login.php > /dev/null
|
||||||
|
|
||||||
|
# Initialize the process
|
||||||
|
# First, delete any existing instance for this person and process to ensure a clean slate
|
||||||
|
curl -s -L -b cookie.txt -X POST -F 'person_id=9' -F 'process_id=4' -F 'delete_existing=1' http://localhost/_init_single_instance.php > /dev/null
|
||||||
|
|
||||||
|
# Get instance details
|
||||||
|
curl -s -L -b cookie.txt "http://localhost/_get_instance_details.php?person_id=9&process_id=4"
|
||||||
Loading…
x
Reference in New Issue
Block a user