Initial import
This commit is contained in:
commit
dc41df7bf6
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
*/node_modules/
|
||||
*/build/
|
||||
18
.htaccess
Normal file
18
.htaccess
Normal file
@ -0,0 +1,18 @@
|
||||
DirectoryIndex index.php index.html
|
||||
Options -Indexes
|
||||
Options -MultiViews
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# 0) Serve existing files/directories as-is
|
||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# 1) Internal map: /page or /page/ -> /page.php (if such PHP file exists)
|
||||
RewriteCond %{REQUEST_FILENAME}.php -f
|
||||
RewriteRule ^(.+?)/?$ $1.php [L]
|
||||
|
||||
# 2) Optional: strip trailing slash for non-directories (keeps .php links working)
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.+)/$ $1 [R=301,L]
|
||||
0
.perm_test_apache
Normal file
0
.perm_test_apache
Normal file
0
.perm_test_exec
Normal file
0
.perm_test_exec
Normal file
863
WorkflowEngine.php
Normal file
863
WorkflowEngine.php
Normal file
@ -0,0 +1,863 @@
|
||||
<?php
|
||||
// Cache-buster: 1720638682
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/lib/WorkflowExceptions.php';
|
||||
|
||||
class WorkflowEngine {
|
||||
|
||||
private $pdo;
|
||||
|
||||
public function __construct() {
|
||||
$this->pdo = db();
|
||||
}
|
||||
|
||||
public function getDashboardMatrix(?string $searchTerm = null, ?int $groupId = null, ?int $activeProcessDefinitionId = null, ?int $meetingFilterGroupId = null, ?string $meetingFilterDatetime = null): array {
|
||||
// 1. Base query for people
|
||||
$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";
|
||||
$params = [];
|
||||
$where_clauses = [];
|
||||
|
||||
// 2. Add filter conditions
|
||||
if ($searchTerm) {
|
||||
$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 ($meetingFilterGroupId && $meetingFilterDatetime) {
|
||||
$meetingId = $this->getOrCreateMeeting($meetingFilterGroupId, $meetingFilterDatetime);
|
||||
$sql_people .= " INNER JOIN meeting_attendance ma ON p.id = ma.person_id";
|
||||
$where_clauses[] = "ma.meeting_id = :meeting_id";
|
||||
$where_clauses[] = "ma.attendance_status IN ('present', 'absent', 'substitute')";
|
||||
$params[':meeting_id'] = $meetingId;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 4. Fetch all process definitions with their JSON
|
||||
$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();
|
||||
$process_definitions_raw = $stmt_defs->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$definitions = [];
|
||||
$definition_map = [];
|
||||
foreach ($process_definitions_raw as $def) {
|
||||
$definitions[$def['id']] = [
|
||||
'id' => $def['id'],
|
||||
'name' => $def['name'],
|
||||
'is_active' => $def['is_active']
|
||||
];
|
||||
$definition_map[$def['id']] = !empty($def['definition_json']) ? json_decode($def['definition_json'], true) : null;
|
||||
}
|
||||
|
||||
// 5. Fetch instances ONLY for the filtered people
|
||||
$instances = [];
|
||||
$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);
|
||||
|
||||
foreach ($instances_data as $instance) {
|
||||
$enriched_instance = $instance;
|
||||
$def_id = $instance['process_definition_id'];
|
||||
$node_id = $instance['current_node_id'];
|
||||
|
||||
$definition = $definition_map[$def_id] ?? null;
|
||||
|
||||
if ($definition && isset($definition['type']) && $definition['type'] === 'checklist') {
|
||||
$tasks = $definition['tasks'] ?? [];
|
||||
$instanceData = $instance['data_json'] ? json_decode($instance['data_json'], true) : [];
|
||||
$totalTasks = count($tasks);
|
||||
$completedTasks = 0;
|
||||
if(is_array($instanceData)) {
|
||||
foreach ($tasks as $task) {
|
||||
if (!empty($instanceData[$task['code']])) {
|
||||
$completedTasks++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($totalTasks > 0 && $completedTasks === $totalTasks) {
|
||||
$status = 'completed';
|
||||
} elseif ($completedTasks > 0) {
|
||||
$status = 'in_progress';
|
||||
} else {
|
||||
$status = 'inactive';
|
||||
}
|
||||
$enriched_instance['computed_status'] = $status;
|
||||
$enriched_instance['computed_reason'] = "$completedTasks/$totalTasks completed";
|
||||
$enriched_instance['computed_next_step'] = '';
|
||||
} else if ($definition && isset($definition['nodes'][$node_id])) {
|
||||
$node_info = $definition['nodes'][$node_id];
|
||||
$enriched_instance['computed_status'] = $node_info['ui_hints']['status'] ?? $instance['current_status'];
|
||||
$enriched_instance['computed_reason'] = $node_info['ui_hints']['reason'] ?? $instance['current_reason'];
|
||||
$enriched_instance['computed_next_step'] = $node_info['ui_hints']['next_step'] ?? $instance['suggested_next_step'];
|
||||
} else {
|
||||
$enriched_instance['computed_status'] = $instance['current_status'];
|
||||
$enriched_instance['computed_reason'] = $instance['current_reason'];
|
||||
$enriched_instance['computed_next_step'] = $instance['suggested_next_step'];
|
||||
}
|
||||
|
||||
$instances[$instance['person_id']][$def_id] = $enriched_instance;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Fetch ancillary data
|
||||
$stmt_functions = $this->pdo->query("SELECT * FROM functions ORDER BY display_order");
|
||||
$all_functions = $stmt_functions->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$stmt_person_functions = $this->pdo->query("SELECT user_id, function_id FROM user_functions");
|
||||
$person_functions_map = [];
|
||||
while ($row = $stmt_person_functions->fetch(PDO::FETCH_ASSOC)) {
|
||||
$person_functions_map[$row['user_id']][] = $row['function_id'];
|
||||
}
|
||||
|
||||
$stmt_bni_groups = $this->pdo->query("SELECT * FROM bni_groups ORDER BY name");
|
||||
$bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 7. Fetch Spotkania columns (upcoming meetings)
|
||||
$today = date('Y-m-d H:i:s');
|
||||
$stmt_meetings = $this->pdo->prepare("
|
||||
WITH RankedMeetings AS (
|
||||
SELECT
|
||||
bg.id as group_id,
|
||||
bg.name as group_name,
|
||||
ce.start_datetime,
|
||||
ROW_NUMBER() OVER(PARTITION BY bg.id ORDER BY ce.start_datetime) as rn
|
||||
FROM bni_groups bg
|
||||
JOIN calendar_event_groups ceg ON bg.id = ceg.bni_group_id
|
||||
JOIN calendar_events ce ON ceg.calendar_event_id = ce.id
|
||||
WHERE ce.start_datetime >= :today
|
||||
)
|
||||
SELECT group_id, group_name, start_datetime
|
||||
FROM RankedMeetings
|
||||
WHERE rn <= 3
|
||||
ORDER BY group_id, start_datetime;
|
||||
");
|
||||
$stmt_meetings->execute(['today' => $today]);
|
||||
$upcoming_meetings_flat = $stmt_meetings->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$spotkania_cols = [];
|
||||
foreach ($upcoming_meetings_flat as $meeting) {
|
||||
$spotkania_cols[$meeting['group_id']]['group_id'] = $meeting['group_id'];
|
||||
$spotkania_cols[$meeting['group_id']]['group_name'] = $meeting['group_name'];
|
||||
$spotkania_cols[$meeting['group_id']]['meetings'][] = $meeting['start_datetime'];
|
||||
}
|
||||
|
||||
|
||||
return [
|
||||
'people' => $people,
|
||||
'definitions' => array_values($definitions),
|
||||
'instances' => $instances,
|
||||
'all_functions' => $all_functions,
|
||||
'person_functions_map' => $person_functions_map,
|
||||
'bni_groups' => $bni_groups,
|
||||
'spotkania_cols' => $spotkania_cols, // Add this to the return array
|
||||
];
|
||||
}
|
||||
|
||||
public function startProcess(string $processCode, int $personId, int $userId): int {
|
||||
$this->pdo->beginTransaction();
|
||||
try {
|
||||
// 1. Find active process definition by code.
|
||||
$stmt_def = $this->pdo->prepare("SELECT * FROM process_definitions WHERE code = ? AND is_active = 1");
|
||||
$stmt_def->execute([$processCode]);
|
||||
$definition = $stmt_def->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$definition) {
|
||||
// If no process definition is found, check if there is a definition for a checklist
|
||||
$stmt_def = $this->pdo->prepare("SELECT * FROM process_definitions WHERE id = ?");
|
||||
$stmt_def->execute([$processCode]);
|
||||
$definition = $stmt_def->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$definition) {
|
||||
throw new WorkflowNotFoundException("Process definition with code or id '$processCode' not found.");
|
||||
}
|
||||
|
||||
$definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : [];
|
||||
if (empty($definition_json) || $definition_json['type'] !== 'checklist') {
|
||||
throw new WorkflowNotAllowedException("Process definition with code '$processCode' not found or not a checklist.");
|
||||
}
|
||||
|
||||
// For checklists, there's no start_node_id, so we can proceed with instance creation
|
||||
$startNodeId = null;
|
||||
|
||||
} else {
|
||||
$definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : [];
|
||||
if (empty($definition_json) || !isset($definition_json['start_node_id'])) {
|
||||
throw new WorkflowRuleFailedException("Process definition is missing start_node_id.");
|
||||
}
|
||||
$startNodeId = $definition_json['start_node_id'];
|
||||
}
|
||||
|
||||
// 2. Create a new process instance.
|
||||
$stmt_insert = $this->pdo->prepare(
|
||||
"INSERT INTO process_instances (person_id, process_definition_id, current_node_id, current_status, last_activity_at) VALUES (?, ?, ?, 'in_progress', NOW())"
|
||||
);
|
||||
$stmt_insert->execute([$personId, $definition['id'], $startNodeId]);
|
||||
$instanceId = $this->pdo->lastInsertId();
|
||||
|
||||
// 3. Create a system event for process start.
|
||||
$this->addEvent($instanceId, 'system', 'Process started.', $startNodeId, [], $userId);
|
||||
|
||||
$this->pdo->commit();
|
||||
return (int)$instanceId;
|
||||
} catch (Exception $e) {
|
||||
$this->pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function getProcessState(int $instanceId): ?array {
|
||||
$stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE id = ?");
|
||||
$stmt->execute([$instanceId]);
|
||||
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$instance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?");
|
||||
$stmt_def->execute([$instance['process_definition_id']]);
|
||||
$definition_json = $stmt_def->fetchColumn();
|
||||
$definition = !empty($definition_json) ? json_decode($definition_json, true) : [];
|
||||
|
||||
$currentNodeId = $instance['current_node_id'];
|
||||
$nodeInfo = $definition['nodes'][$currentNodeId] ?? null;
|
||||
|
||||
return [
|
||||
'instance' => $instance,
|
||||
'definition' => $definition,
|
||||
'currentNode' => $nodeInfo,
|
||||
];
|
||||
}
|
||||
|
||||
public function applyTransition(int $instanceId, string $transitionId, array $inputPayload, int $userId): array {
|
||||
$this->pdo->beginTransaction();
|
||||
try {
|
||||
$state = $this->getProcessState($instanceId);
|
||||
if (!$state) {
|
||||
throw new WorkflowNotFoundException("Process instance not found.");
|
||||
}
|
||||
|
||||
$instance = $state['instance'];
|
||||
$definition = $state['definition'];
|
||||
$currentNodeId = $instance['current_node_id'];
|
||||
|
||||
$transition = null;
|
||||
foreach ($definition['transitions'] as $t) {
|
||||
if ($t['from'] === $currentNodeId && $t['id'] === $transitionId) {
|
||||
// For user-triggered transitions, we ignore the condition here.
|
||||
// The UI should prevent showing buttons for transitions whose data-based conditions aren't met.
|
||||
$transition = $t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$transition) {
|
||||
throw new WorkflowNotAllowedException("Transition not found or not allowed from the current node.");
|
||||
}
|
||||
|
||||
// Apply the initial, user-triggered transition
|
||||
$newNodeId = $this->applySingleTransition($instanceId, $instance, $definition, $transition, $inputPayload, $userId);
|
||||
$instance['current_node_id'] = $newNodeId; // Update instance state for the loop
|
||||
|
||||
// Loop for automatic transitions (router nodes)
|
||||
for ($i = 0; $i < 10; $i++) { // Max 10 auto-steps to prevent infinite loops
|
||||
$autoTransition = $this->findAutomaticTransition($instance, $definition);
|
||||
if ($autoTransition) {
|
||||
// Automatic transitions have no user payload
|
||||
$newNodeId = $this->applySingleTransition($instanceId, $instance, $definition, $autoTransition, [], $userId);
|
||||
$instance['current_node_id'] = $newNodeId; // Update for next iteration
|
||||
} else {
|
||||
break; // No more automatic transitions found
|
||||
}
|
||||
}
|
||||
|
||||
$this->pdo->commit();
|
||||
|
||||
// Refetch the final state of the instance to return the correct status
|
||||
$finalState = $this->getProcessState($instanceId)['instance'];
|
||||
$finalNodeInfo = $definition['nodes'][$finalState['current_node_id']] ?? null;
|
||||
|
||||
return [
|
||||
'instanceId' => $instanceId,
|
||||
'currentNodeId' => $finalState['current_node_id'],
|
||||
'currentStatus' => $finalNodeInfo['ui_hints']['status'] ?? $finalState['current_status'],
|
||||
'currentReason' => $finalNodeInfo['ui_hints']['reason'] ?? $finalState['current_reason'],
|
||||
'suggestedNextStep' => $finalNodeInfo['ui_hints']['next_step'] ?? $finalState['suggested_next_step'],
|
||||
'lastActivityAt' => $finalState['last_activity_at'],
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function applySingleTransition(int $instanceId, array &$instance, array $definition, array $transition, array $inputPayload, int $userId): string
|
||||
{
|
||||
$newNodeId = $transition['to'];
|
||||
$newNodeInfo = $definition['nodes'][$newNodeId] ?? null;
|
||||
|
||||
$newStatus = $newNodeInfo['ui_hints']['status'] ?? 'in_progress';
|
||||
$newReason = $newNodeInfo['ui_hints']['reason'] ?? '';
|
||||
$newNextStep = $newNodeInfo['ui_hints']['next_step'] ?? '';
|
||||
|
||||
$stmt_update = $this->pdo->prepare(
|
||||
"UPDATE process_instances SET current_node_id = ?, current_status = ?, current_reason = ?, suggested_next_step = ?, last_activity_at = NOW() WHERE id = ?"
|
||||
);
|
||||
$stmt_update->execute([
|
||||
$newNodeId,
|
||||
$newStatus,
|
||||
$newReason,
|
||||
$newNextStep,
|
||||
$instanceId
|
||||
]);
|
||||
|
||||
$message = $inputPayload['message'] ?? $transition['name'];
|
||||
$this->addEvent($instanceId, 'transition_applied', $message, $newNodeId, $inputPayload, $userId);
|
||||
|
||||
if (isset($transition['actions'])) {
|
||||
foreach ($transition['actions'] as $action) {
|
||||
if ($action['type'] === 'start_process') {
|
||||
$this->executeStartProcessAction($instance['person_id'], $action, $userId);
|
||||
} elseif ($action['type'] === 'set_data') {
|
||||
// Pass the instance by reference to be updated with new data
|
||||
$this->executeSetDataAction($instanceId, $instance, $action, $inputPayload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $newNodeId;
|
||||
}
|
||||
|
||||
private function findAutomaticTransition(array $instance, array $definition): ?array
|
||||
{
|
||||
$currentNodeId = $instance['current_node_id'];
|
||||
foreach ($definition['transitions'] as $transition) {
|
||||
if ($transition['from'] === $currentNodeId) {
|
||||
// An automatic transition MUST have a condition.
|
||||
if (isset($transition['condition'])) {
|
||||
if ($this->checkTransitionCondition($transition, $instance)) {
|
||||
return $transition;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function addNote(int $instanceId, string $message, int $userId): bool {
|
||||
$state = $this->getProcessState($instanceId);
|
||||
if (!$state) {
|
||||
throw new WorkflowNotFoundException("Process instance #$instanceId not found.");
|
||||
}
|
||||
$currentNodeId = $state['instance']['current_node_id'];
|
||||
$payload = ['message' => $message];
|
||||
$this->addEvent($instanceId, 'note', $message, $currentNodeId, $payload, $userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function applyManualStatus(int $instanceId, string $status, string $reasonOrNote, int $userId): bool {
|
||||
$this->pdo->beginTransaction();
|
||||
try {
|
||||
$state = $this->getProcessState($instanceId);
|
||||
if (!$state) {
|
||||
throw new WorkflowNotFoundException("Process instance #$instanceId not found.");
|
||||
}
|
||||
|
||||
$stmt_update = $this->pdo->prepare(
|
||||
"UPDATE process_instances SET current_status = ?, current_reason = ?, last_activity_at = NOW() WHERE id = ?"
|
||||
);
|
||||
$stmt_update->execute([$status, $reasonOrNote, $instanceId]);
|
||||
|
||||
$currentNodeId = $state['instance']['current_node_id'];
|
||||
$message = "Status manually set to '$status'.";
|
||||
if (!empty($reasonOrNote)) {
|
||||
$message .= " Reason: $reasonOrNote";
|
||||
}
|
||||
|
||||
$this->addEvent($instanceId, 'manual_status_change', $message, $currentNodeId, ['status' => $status, 'reason' => $reasonOrNote], $userId);
|
||||
|
||||
$this->pdo->commit();
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
$this->pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function bulkAddNotes(array $notes): array
|
||||
{
|
||||
$results = [];
|
||||
foreach ($notes as $note) {
|
||||
try {
|
||||
$this->addNote((int)$note['instance_id'], $note['message'], (int)$note['user_id']);
|
||||
$results[] = ['instance_id' => $note['instance_id'], 'success' => true];
|
||||
} catch (Exception $e) {
|
||||
$results[] = ['instance_id' => $note['instance_id'], 'success' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function bulkManualStatus(array $statuses): array
|
||||
{
|
||||
$results = [];
|
||||
foreach ($statuses as $status) {
|
||||
try {
|
||||
$this->applyManualStatus((int)$status['instance_id'], $status['status'], $status['reason'] ?? '', (int)$status['user_id']);
|
||||
$results[] = ['instance_id' => $status['instance_id'], 'success' => true];
|
||||
} catch (Exception $e) {
|
||||
$results[] = ['instance_id' => $status['instance_id'], 'success' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function updateChecklistStatus(int $instanceId, string $taskCode, bool $isChecked, int $userId): array
|
||||
{
|
||||
$this->pdo->beginTransaction();
|
||||
try {
|
||||
// Get current data_json
|
||||
$stmt = $this->pdo->prepare("SELECT data_json, process_definition_id FROM process_instances WHERE id = ?");
|
||||
$stmt->execute([$instanceId]);
|
||||
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$instance) {
|
||||
throw new WorkflowNotFoundException("Process instance #$instanceId not found.");
|
||||
}
|
||||
|
||||
$data = $instance['data_json'] ? json_decode($instance['data_json'], true) : [];
|
||||
|
||||
// Update the specific task status
|
||||
$data[$taskCode] = $isChecked;
|
||||
$newDataJson = json_encode($data);
|
||||
|
||||
// Save new data_json and update timestamp
|
||||
$stmt = $this->pdo->prepare("UPDATE process_instances SET data_json = ?, last_activity_at = CURRENT_TIMESTAMP WHERE id = ?");
|
||||
$stmt->execute([$newDataJson, $instanceId]);
|
||||
|
||||
// Add an event for the checklist update
|
||||
$message = "Checklist task '$taskCode' marked as " . ($isChecked ? 'complete' : 'incomplete') . ".";
|
||||
$this->addEvent($instanceId, 'checklist_update', $message, null, ['task' => $taskCode, 'checked' => $isChecked], $userId);
|
||||
|
||||
// Calculate progress
|
||||
$stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?");
|
||||
$stmt_def->execute([$instance['process_definition_id']]);
|
||||
$definitionJson = $stmt_def->fetchColumn();
|
||||
$definition = json_decode($definitionJson, true);
|
||||
$totalTasks = count($definition['tasks'] ?? []);
|
||||
$completedTasks = count(array_filter($data));
|
||||
|
||||
$this->pdo->commit();
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'progress' => [
|
||||
'completed' => $completedTasks,
|
||||
'total' => $totalTasks
|
||||
],
|
||||
'lastActivityAt' => date('d/m/y')
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function addEvent(int $instanceId, string $eventType, string $message, ?string $nodeId, array $payload, int $userId): void {
|
||||
$stmt = $this->pdo->prepare(
|
||||
"INSERT INTO process_events (process_instance_id, event_type, message, node_id, payload_json, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())"
|
||||
);
|
||||
$stmt->execute([$instanceId, $eventType, $message, $nodeId, json_encode($payload), $userId]);
|
||||
}
|
||||
|
||||
public function getOrCreateInstanceByDefId(int $personId, int $processDefinitionId, int $userId, array $context = []): ?array {
|
||||
if (!is_int($processDefinitionId) || $processDefinitionId <= 0) {
|
||||
throw new InvalidArgumentException("processDefinitionId must be a positive integer.");
|
||||
}
|
||||
if (!is_int($personId) || $personId <= 0) {
|
||||
throw new InvalidArgumentException("personId must be a positive integer.");
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE `person_id` = ? AND `process_definition_id` = ?");
|
||||
$stmt->execute([$personId, $processDefinitionId]);
|
||||
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$instance) {
|
||||
$stmt_def = $this->pdo->prepare("SELECT definition_json, code, is_active FROM process_definitions WHERE id = ?");
|
||||
$stmt_def->execute([$processDefinitionId]);
|
||||
$definition = $stmt_def->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$definition) {
|
||||
throw new WorkflowNotFoundException("Process definition #$processDefinitionId not found.");
|
||||
}
|
||||
|
||||
if (empty($definition['is_active'])) {
|
||||
throw new WorkflowNotAllowedException("Process is not active and cannot be started.");
|
||||
}
|
||||
|
||||
$eligibility = $this->checkEligibility($personId, $processDefinitionId, $context);
|
||||
if (!$eligibility['is_eligible']) {
|
||||
throw new WorkflowEligibilityException("Person is not eligible to start this process.", $eligibility['reasons']);
|
||||
}
|
||||
|
||||
$definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : [];
|
||||
|
||||
$processCode = ($definition_json && isset($definition_json['type']) && $definition_json['type'] === 'checklist')
|
||||
? (string) $processDefinitionId
|
||||
: $definition['code'];
|
||||
|
||||
if ($processCode) {
|
||||
$instanceId = $this->startProcess($processCode, $personId, $userId);
|
||||
if ($instanceId) {
|
||||
$stmt->execute([$personId, $processDefinitionId]);
|
||||
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $instance ?: null;
|
||||
}
|
||||
|
||||
public function getInstanceByDefId(int $personId, int $processDefinitionId): ?array {
|
||||
$stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE `person_id` = ? AND `process_definition_id` = ?");
|
||||
$stmt->execute([$personId, $processDefinitionId]);
|
||||
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return $instance ?: null;
|
||||
}
|
||||
|
||||
public function getEvents(int $instanceId): array {
|
||||
$stmt_events = $this->pdo->prepare("SELECT pe.*, p.email as user_email, p.first_name, p.last_name FROM process_events pe JOIN people p ON pe.created_by = p.id WHERE pe.process_instance_id = ? ORDER BY pe.created_at DESC");
|
||||
$stmt_events->execute([$instanceId]);
|
||||
return $stmt_events->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public function getAvailableTransitions(int $instanceId): array {
|
||||
$state = $this->getProcessState($instanceId);
|
||||
if (!$state) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$currentNodeId = $state['instance']['current_node_id'];
|
||||
$definition = $state['definition'];
|
||||
|
||||
$transitions = [];
|
||||
if (isset($definition['transitions'])) {
|
||||
foreach ($definition['transitions'] as $t) {
|
||||
if ($t['from'] === $currentNodeId) {
|
||||
$transitions[] = $t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $transitions;
|
||||
}
|
||||
|
||||
public function getProcessDefinitionNodes(int $processDefinitionId): array {
|
||||
$stmt = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?");
|
||||
$stmt->execute([$processDefinitionId]);
|
||||
$json = $stmt->fetchColumn();
|
||||
|
||||
if (!$json) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$definition = !empty($json) ? json_decode($json, true) : [];
|
||||
return $definition['nodes'] ?? [];
|
||||
}
|
||||
|
||||
public function checkEligibility(int $personId, int $processDefinitionId, array $context = []): array {
|
||||
$stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?");
|
||||
$stmt_def->execute([$processDefinitionId]);
|
||||
$definition_json = $stmt_def->fetchColumn();
|
||||
|
||||
$definition = !empty($definition_json) ? json_decode($definition_json, true) : [];
|
||||
|
||||
$reasons = [];
|
||||
if (empty($definition) || empty($definition['eligibility_rules'])) {
|
||||
return ['is_eligible' => true, 'reasons' => []];
|
||||
}
|
||||
|
||||
foreach ($definition['eligibility_rules'] as $rule) {
|
||||
try {
|
||||
$params = $rule['params'] ?? $rule;
|
||||
switch ($rule['type']) {
|
||||
case 'checkProcessCompletedRule':
|
||||
case 'process_completed': // Backward compatibility
|
||||
$this->checkProcessCompletedRule($personId, $params);
|
||||
break;
|
||||
case 'checkProcessDataRule':
|
||||
$this->checkProcessDataRule($personId, $params);
|
||||
break;
|
||||
case 'deny_manual_start':
|
||||
$this->checkDenyManualStartRule($context);
|
||||
break;
|
||||
case 'person_property_equals':
|
||||
$this->checkPersonPropertyEqualsRule($personId, $params);
|
||||
break;
|
||||
// Add other rule types here
|
||||
}
|
||||
} catch (WorkflowNotAllowedException $e) {
|
||||
$reasons[] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return ['is_eligible' => empty($reasons), 'reasons' => $reasons];
|
||||
}
|
||||
|
||||
private function checkDenyManualStartRule(array $context): void {
|
||||
if (!isset($context['source']) || $context['source'] !== 'chain') {
|
||||
throw new WorkflowNotAllowedException("This process can only be started automatically by another process.");
|
||||
}
|
||||
}
|
||||
|
||||
private function checkPersonPropertyEqualsRule(int $personId, array $params): void {
|
||||
$stmt = $this->pdo->prepare("SELECT * FROM people WHERE id = ?");
|
||||
$stmt->execute([$personId]);
|
||||
$person = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$person) {
|
||||
throw new WorkflowNotAllowedException("Person not found.");
|
||||
}
|
||||
|
||||
$property = $params['property'];
|
||||
$expectedValue = $params['value'];
|
||||
|
||||
if (!isset($person[$property])) {
|
||||
throw new WorkflowNotAllowedException("Property '{$property}' not found on person.");
|
||||
}
|
||||
|
||||
if ($person[$property] !== $expectedValue) {
|
||||
throw new WorkflowNotAllowedException("Person's property '{$property}' is not '{$expectedValue}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private function checkProcessCompletedRule(int $personId, array $params): void {
|
||||
$stmt = $this->pdo->prepare("\n SELECT pi.id\n FROM process_instances pi\n JOIN process_definitions pd ON pi.process_definition_id = pd.id\n WHERE pi.person_id = ? AND pd.code = ? AND pi.current_status = ?\n ORDER BY pi.last_activity_at DESC\n LIMIT 1\n ");
|
||||
$stmt->execute([$personId, $params['process_code'], $params['expected_status']]);
|
||||
$instance = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$instance) {
|
||||
throw new WorkflowNotAllowedException("Prerequisite process '{$params['process_code']}' not completed with status '{$params['expected_status']}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private function checkProcessDataRule(int $personId, array $params): void {
|
||||
$stmt = $this->pdo->prepare("
|
||||
SELECT pi.data_json
|
||||
FROM process_instances pi
|
||||
JOIN process_definitions pd ON pi.process_definition_id = pd.id
|
||||
WHERE pi.person_id = ? AND pd.code = ? AND pi.current_status = ?
|
||||
ORDER BY pi.last_activity_at DESC
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$personId, $params['process_code'], $params['expected_status']]);
|
||||
$data_json = $stmt->fetchColumn();
|
||||
|
||||
if (!$data_json) {
|
||||
throw new WorkflowNotAllowedException("Not eligible to start this process. Prerequisite process '{$params['process_code']}' not found with status '{$params['expected_status']}'.");
|
||||
}
|
||||
|
||||
$data = json_decode($data_json, true);
|
||||
if (!is_array($data)) {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
foreach ($params['expected_data'] as $key => $expected_value) {
|
||||
if (!isset($data[$key]) || $data[$key] !== $expected_value) {
|
||||
throw new WorkflowNotAllowedException("Not eligible. Condition not met: '$key' is not '$expected_value'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function checkTransitionCondition(array $transition, array $instanceData): bool
|
||||
{
|
||||
if (!isset($transition['condition'])) {
|
||||
// A transition without a condition is not automatic, but it is valid to pass through.
|
||||
// The calling context (findAutomaticTransition) will decide if this is an error.
|
||||
return true;
|
||||
}
|
||||
|
||||
$condition = $transition['condition'];
|
||||
$data = isset($instanceData['data_json']) ? json_decode($instanceData['data_json'], true) : [];
|
||||
|
||||
$field = $condition['field'] ?? null;
|
||||
$expectedValue = $condition['value'] ?? null;
|
||||
|
||||
if ($field === null || $expectedValue === null) {
|
||||
// Malformed condition
|
||||
return false;
|
||||
}
|
||||
|
||||
return isset($data[$field]) && $data[$field] === $expectedValue;
|
||||
}
|
||||
|
||||
private function executeStartProcessAction(int $personId, array $action, int $userId): void {
|
||||
$stmt = $this->pdo->prepare("SELECT id FROM process_definitions WHERE code = ?");
|
||||
$stmt->execute([$action['process_code']]);
|
||||
$processDefinitionId = $stmt->fetchColumn();
|
||||
|
||||
if ($processDefinitionId) {
|
||||
$this->getOrCreateInstanceByDefId($personId, $processDefinitionId, $userId, ['source' => 'chain']);
|
||||
}
|
||||
}
|
||||
|
||||
private function executeSetDataAction(int $instanceId, array &$instance, array $action, array $payload): void {
|
||||
$dataJson = $instance['data_json'];
|
||||
$data = $dataJson ? json_decode($dataJson, true) : [];
|
||||
|
||||
if (isset($action['params']['keys']) && is_array($action['params']['keys'])) {
|
||||
foreach ($action['params']['keys'] as $key) {
|
||||
if (array_key_exists($key, $payload)) {
|
||||
$data[$key] = $payload[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$newDataJson = json_encode($data);
|
||||
|
||||
// Update the database
|
||||
$stmt_update = $this->pdo->prepare("UPDATE process_instances SET data_json = ? WHERE id = ?");
|
||||
$stmt_update->execute([$newDataJson, $instanceId]);
|
||||
|
||||
// Also update the in-memory instance for the next step in the chain
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
public function getOrCreateMeeting(int $bniGroupId, string $meetingDatetime): int {
|
||||
$meetingKey = $bniGroupId . '_' . $meetingDatetime;
|
||||
$stmt = $this->pdo->prepare("SELECT id FROM meetings WHERE meeting_key = ?");
|
||||
$stmt->execute([$meetingKey]);
|
||||
$meetingId = $stmt->fetchColumn();
|
||||
|
||||
if (!$meetingId) {
|
||||
$stmt = $this->pdo->prepare("INSERT INTO meetings (bni_group_id, meeting_date, meeting_datetime, meeting_key) VALUES (?, DATE(?), ?, ?)");
|
||||
$stmt->execute([$bniGroupId, $meetingDatetime, $meetingDatetime, $meetingKey]);
|
||||
$meetingId = $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
return (int)$meetingId;
|
||||
}
|
||||
|
||||
public function getMeetingAttendance(int $meetingId): array {
|
||||
$stmt = $this->pdo->prepare("SELECT * FROM meeting_attendance WHERE meeting_id = ?");
|
||||
$stmt->execute([$meetingId]);
|
||||
$attendance_raw = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$attendance = [];
|
||||
foreach ($attendance_raw as $att) {
|
||||
$attendance[$att['person_id']] = $att;
|
||||
}
|
||||
|
||||
return $attendance;
|
||||
}
|
||||
|
||||
public function updateMeetingAttendance(int $meetingId, int $personId, int $bniGroupId, string $status, int $userId, ?string $guestSurvey = null): void {
|
||||
$sql = "INSERT INTO meeting_attendance (meeting_id, person_id, bni_group_id, attendance_status, guest_survey, updated_by) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE attendance_status = VALUES(attendance_status), guest_survey = VALUES(guest_survey), updated_by = VALUES(updated_by), bni_group_id = VALUES(bni_group_id)";
|
||||
|
||||
// Log query and params
|
||||
$params = [$meetingId, $personId, $bniGroupId, $status, $guestSurvey, $userId];
|
||||
error_log("SQL: $sql");
|
||||
error_log("Params: " . json_encode($params));
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
}
|
||||
|
||||
public function getMeetingAttendanceByGroupAndDate(int $groupId, string $meetingDatetime): array {
|
||||
$meetingId = $this->getOrCreateMeeting($groupId, $meetingDatetime);
|
||||
return $this->getMeetingAttendance($meetingId);
|
||||
}
|
||||
|
||||
public function isMemberOfGroup(int $personId, int $bniGroupId): bool {
|
||||
$stmt = $this->pdo->prepare("SELECT COUNT(*) FROM people WHERE id = ? AND bni_group_id = ?");
|
||||
$stmt->execute([$personId, $bniGroupId]);
|
||||
return (int)$stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
public function getMeetingDetails(int $personId, int $bniGroupId, string $meetingDatetime): array {
|
||||
$meetingId = $this->getOrCreateMeeting($bniGroupId, $meetingDatetime);
|
||||
|
||||
$stmt = $this->pdo->prepare("SELECT * FROM meeting_attendance WHERE meeting_id = ? AND person_id = ?");
|
||||
$stmt->execute([$meetingId, $personId]);
|
||||
$attendance = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($attendance) {
|
||||
return $attendance;
|
||||
}
|
||||
|
||||
// If no record, return default state
|
||||
$isMember = $this->isMemberOfGroup($personId, $bniGroupId);
|
||||
return [
|
||||
'meeting_id' => $meetingId,
|
||||
'person_id' => $personId,
|
||||
'attendance_status' => $isMember ? 'present' : 'none',
|
||||
'guest_survey' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function getPeopleDetails(array $personIds): array {
|
||||
if (empty($personIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($personIds), '?'));
|
||||
|
||||
$sql = "SELECT p.id, p.first_name, p.last_name, p.company_name, p.industry, bg.name as bni_group_name
|
||||
FROM people p
|
||||
LEFT JOIN bni_groups bg ON p.bni_group_id = bg.id
|
||||
WHERE p.id IN ($placeholders)";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($personIds);
|
||||
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
32
_add_bni_group.php
Normal file
32
_add_bni_group.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
if (isset($_POST['add'])) {
|
||||
$name = $_POST['name'];
|
||||
$city = $_POST['city'] ?? null;
|
||||
$active = isset($_POST['active']) ? 1 : 0;
|
||||
$display_order = $_POST['display_order'] ?? 0;
|
||||
|
||||
if (empty($name)) {
|
||||
$_SESSION['error_message'] = 'Name is required.';
|
||||
header('Location: bni_groups.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("INSERT INTO bni_groups (name, city, active, display_order) VALUES (:name, :city, :active, :display_order)");
|
||||
$stmt->bindParam(':name', $name);
|
||||
$stmt->bindParam(':city', $city);
|
||||
$stmt->bindParam(':active', $active, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':display_order', $display_order, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
$_SESSION['success_message'] = 'BNI Group added successfully!';
|
||||
} catch (PDOException $e) {
|
||||
$_SESSION['error_message'] = 'Error adding BNI group: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: bni_groups.php');
|
||||
exit;
|
||||
104
_add_calendar_event.php
Normal file
104
_add_calendar_event.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header("Location: login.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once 'db/config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$title = $_POST['title'] ?? '';
|
||||
$description = $_POST['description'] ?? '';
|
||||
$start_datetime = $_POST['start_datetime'] ?? '';
|
||||
$end_datetime = $_POST['end_datetime'] ?? '';
|
||||
$event_type_id = $_POST['event_type_id'] ?? null;
|
||||
$recurrence = $_POST['recurrence'] ?? '';
|
||||
$recurrence_end_date = $_POST['recurrence_end_date'] ?? '';
|
||||
|
||||
if (empty($recurrence)) {
|
||||
$recurrence = null;
|
||||
}
|
||||
if (empty($recurrence_end_date)) {
|
||||
$recurrence_end_date = null;
|
||||
}
|
||||
|
||||
if (empty($title) || empty($start_datetime) || empty($end_datetime) || empty($event_type_id)) {
|
||||
header("Location: calendar.php?error=empty_fields");
|
||||
exit();
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Insert the main event
|
||||
$stmt = $pdo->prepare("INSERT INTO calendar_events (title, description, start_datetime, end_datetime, event_type_id, recurrence, recurrence_end_date) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$title, $description, $start_datetime, $end_datetime, $event_type_id, $recurrence, $recurrence_end_date]);
|
||||
$parent_event_id = $pdo->lastInsertId();
|
||||
|
||||
// Handle group associations
|
||||
if (isset($_POST['group_ids']) && is_array($_POST['group_ids'])) {
|
||||
$stmt_groups = $pdo->prepare("INSERT INTO calendar_event_groups (calendar_event_id, bni_group_id) VALUES (?, ?)");
|
||||
foreach ($_POST['group_ids'] as $group_id) {
|
||||
$stmt_groups->execute([$parent_event_id, $group_id]);
|
||||
}
|
||||
} else {
|
||||
// The field is required, so this is a failure case.
|
||||
throw new Exception("Group IDs are required.");
|
||||
}
|
||||
|
||||
if ($recurrence && !empty($recurrence_end_date)) {
|
||||
$start_date = new DateTime($start_datetime);
|
||||
$end_date = new DateTime($end_datetime);
|
||||
$recurrence_end = new DateTime($recurrence_end_date);
|
||||
$interval_spec = '';
|
||||
|
||||
switch ($recurrence) {
|
||||
case 'daily':
|
||||
$interval_spec = 'P1D';
|
||||
break;
|
||||
case 'weekly':
|
||||
$interval_spec = 'P1W';
|
||||
break;
|
||||
case 'monthly':
|
||||
$interval_spec = 'P1M';
|
||||
break;
|
||||
}
|
||||
|
||||
if ($interval_spec) {
|
||||
$interval = new DateInterval($interval_spec);
|
||||
$period_start = clone $start_date;
|
||||
$period_start->add($interval);
|
||||
|
||||
$period = new DatePeriod($period_start, $interval, $recurrence_end);
|
||||
|
||||
$stmt_recur = $pdo->prepare("INSERT INTO calendar_events (title, description, start_datetime, end_datetime, event_type_id, parent_event_id) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
$stmt_recur_groups = $pdo->prepare("INSERT INTO calendar_event_groups (calendar_event_id, bni_group_id) VALUES (?, ?)");
|
||||
|
||||
foreach ($period as $date) {
|
||||
$new_start_datetime = $date->format('Y-m-d H:i:s');
|
||||
$end_date_clone = clone $date;
|
||||
$new_end_datetime = $end_date_clone->add($start_date->diff($end_date))->format('Y-m-d H:i:s');
|
||||
$stmt_recur->execute([$title, $description, $new_start_datetime, $new_end_datetime, $event_type_id, $parent_event_id]);
|
||||
$new_event_id = $pdo->lastInsertId();
|
||||
foreach ($_POST['group_ids'] as $group_id) {
|
||||
$stmt_recur_groups->execute([$new_event_id, $group_id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
header("Location: calendar.php");
|
||||
exit();
|
||||
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
error_log($e->getMessage());
|
||||
header("Location: calendar.php?error=db_error");
|
||||
exit();
|
||||
}
|
||||
}
|
||||
18
_add_event_type.php
Normal file
18
_add_event_type.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
|
||||
if (isset($_POST['add'])) {
|
||||
$name = $_POST['name'];
|
||||
$color = $_POST['color'];
|
||||
$display_order = $_POST['display_order'];
|
||||
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("INSERT INTO event_types (name, color, display_order) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$name, $color, $display_order]);
|
||||
|
||||
session_start();
|
||||
$_SESSION['success_message'] = 'Event type added successfully.';
|
||||
header('Location: event_types.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
35
_add_function.php
Normal file
35
_add_function.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['user_id']) || $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: login.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once 'db/config.php';
|
||||
|
||||
if (isset($_POST['name'], $_POST['bni_group_id'])) {
|
||||
$name = trim($_POST['name']);
|
||||
$bni_group_id = trim($_POST['bni_group_id']);
|
||||
|
||||
if (!empty($name) && !empty($bni_group_id)) {
|
||||
try {
|
||||
$pdo = db();
|
||||
// Get the current max display order
|
||||
$stmt = $pdo->query("SELECT MAX(display_order) FROM functions");
|
||||
$max_order = $stmt->fetchColumn();
|
||||
$new_order = $max_order + 1;
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO functions (name, bni_group_id, display_order) VALUES (:name, :bni_group_id, :display_order)");
|
||||
$stmt->execute(['name' => $name, 'bni_group_id' => $bni_group_id, 'display_order' => $new_order]);
|
||||
$_SESSION['success_message'] = "Function added successfully.";
|
||||
} catch (PDOException $e) {
|
||||
// Handle potential errors, e.g., duplicate name
|
||||
$_SESSION['error_message'] = "Error adding function: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$_SESSION['error_message'] = "Name and group are required.";
|
||||
}
|
||||
|
||||
header('Location: functions.php');
|
||||
exit();
|
||||
28
_add_process_event.php
Normal file
28
_add_process_event.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
require_once 'lib/ErrorHandler.php';
|
||||
require_once 'WorkflowEngine.php';
|
||||
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
throw new WorkflowNotAllowedException('Unauthorized');
|
||||
}
|
||||
|
||||
$instanceId = $_POST['instance_id'] ?? null;
|
||||
$message = $_POST['message'] ?? null;
|
||||
$userId = $_SESSION['user_id'];
|
||||
|
||||
if (!$instanceId || !$message) {
|
||||
throw new WorkflowRuleFailedException('Missing parameters: instance_id and message are required.');
|
||||
}
|
||||
|
||||
$workflowEngine = new WorkflowEngine();
|
||||
$workflowEngine->addNote((int)$instanceId, $message, (int)$userId);
|
||||
|
||||
if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['message' => 'Note added successfully.']);
|
||||
} else {
|
||||
header('Location: ' . $_SERVER['HTTP_REFERER']);
|
||||
}
|
||||
exit;
|
||||
41
_apply_transition.php
Normal file
41
_apply_transition.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once 'lib/ErrorHandler.php';
|
||||
register_error_handler();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
throw new WorkflowNotAllowedException('Invalid request method.');
|
||||
}
|
||||
|
||||
require_once 'WorkflowEngine.php';
|
||||
|
||||
if (!isset($_POST['instanceId']) || !isset($_POST['transitionId'])) {
|
||||
throw new WorkflowNotAllowedException('Błąd: Brak wymaganych parametrów.');
|
||||
}
|
||||
|
||||
$instanceId = (int)$_POST['instanceId'];
|
||||
$transitionId = $_POST['transitionId'];
|
||||
$userId = $_SESSION['user_id'] ?? null;
|
||||
$payload = $_POST['payload'] ?? [];
|
||||
|
||||
if (!$userId) {
|
||||
throw new WorkflowNotAllowedException('Błąd: Sesja wygasła.', [], 401);
|
||||
}
|
||||
|
||||
$engine = new WorkflowEngine();
|
||||
|
||||
if ($transitionId === 'note') {
|
||||
$message = $payload['message'] ?? '';
|
||||
if (empty($message)) {
|
||||
throw new WorkflowNotAllowedException('Treść notatki nie może być pusta.');
|
||||
}
|
||||
$engine->addNote($instanceId, $message, $userId);
|
||||
$response = ['success' => true, 'message' => 'Notatka została dodana.'];
|
||||
} else {
|
||||
$result = $engine->applyTransition($instanceId, $transitionId, $payload, $userId);
|
||||
$response = ['success' => true, 'message' => 'Akcja została wykonana pomyślnie.', 'data' => $result];
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
50
_bulk_add_event.php
Normal file
50
_bulk_add_event.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
require_once 'lib/ErrorHandler.php';
|
||||
require_once 'WorkflowEngine.php';
|
||||
require_once 'db/config.php';
|
||||
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
throw new WorkflowNotAllowedException('Unauthorized');
|
||||
}
|
||||
|
||||
$person_ids = json_decode($_POST['person_ids'] ?? '[]');
|
||||
$process_id = $_POST['process_id'] ?? null;
|
||||
$message = $_POST['description'] ?? null;
|
||||
$userId = $_SESSION['user_id'];
|
||||
|
||||
if (empty($person_ids) || !$process_id || !$message) {
|
||||
throw new WorkflowRuleFailedException('Missing parameters: person_ids, process_id, and description are required.');
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
$placeholders = implode(',', array_fill(0, count($person_ids), '?'));
|
||||
$stmt = $pdo->prepare("SELECT id FROM process_instances WHERE process_definition_id = ? AND person_id IN ($placeholders)");
|
||||
$params = array_merge([$process_id], $person_ids);
|
||||
$stmt->execute($params);
|
||||
$instance_ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (empty($instance_ids)) {
|
||||
$_SESSION['flash_message'] = "No instances found for the selected people and process.";
|
||||
header('Location: ' . $_SERVER['HTTP_REFERER']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$notes = [];
|
||||
foreach ($instance_ids as $instance_id) {
|
||||
$notes[] = [
|
||||
'instance_id' => $instance_id,
|
||||
'message' => $message,
|
||||
'user_id' => $userId
|
||||
];
|
||||
}
|
||||
|
||||
$workflowEngine = new WorkflowEngine();
|
||||
$results = $workflowEngine->bulkAddNotes($notes);
|
||||
|
||||
$_SESSION['flash_message'] = "Bulk note addition completed.";
|
||||
$_SESSION['bulk_results'] = $results;
|
||||
|
||||
header('Location: ' . $_SERVER['HTTP_REFERER']);
|
||||
exit;
|
||||
56
_bulk_init_instances.php
Normal file
56
_bulk_init_instances.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
require_once 'WorkflowEngine.php';
|
||||
require_once 'lib/ErrorHandler.php';
|
||||
require_once 'lib/WorkflowExceptions.php';
|
||||
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
throw new WorkflowNotAllowedException('Unauthorized');
|
||||
}
|
||||
|
||||
$userId = $_SESSION['user_id'];
|
||||
$personIds = $_POST['personIds'] ?? '[]';
|
||||
if (is_string($personIds)) {
|
||||
$personIds = json_decode($personIds, true);
|
||||
}
|
||||
$process_id = $_POST['process_id'] ?? null;
|
||||
|
||||
if (empty($personIds) || !$process_id) {
|
||||
throw new WorkflowRuleFailedException('Missing parameters');
|
||||
}
|
||||
|
||||
$engine = new WorkflowEngine();
|
||||
$results = [
|
||||
'success' => [],
|
||||
'failed' => [],
|
||||
];
|
||||
|
||||
foreach ($personIds as $personId) {
|
||||
try {
|
||||
$instance = $engine->getOrCreateInstanceByDefId($personId, $process_id, $userId);
|
||||
if ($instance) {
|
||||
$results['success'][] = $personId;
|
||||
} else {
|
||||
$results['failed'][] = $personId;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$results['failed'][] = $personId;
|
||||
// Optionally log the error
|
||||
error_log("Failed to initialize process for person $personId: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$message = "Bulk initialization completed. Success: " . count($results['success']) . ", Failed: " . count($results['failed']);
|
||||
|
||||
if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'message' => $message,
|
||||
'results' => $results
|
||||
]);
|
||||
} else {
|
||||
$_SESSION['success_message'] = $message;
|
||||
header('Location: index.php');
|
||||
}
|
||||
exit();
|
||||
112
_bulk_print_attendance_list.php
Normal file
112
_bulk_print_attendance_list.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
require_once 'lib/ErrorHandler.php';
|
||||
register_error_handler();
|
||||
|
||||
session_start();
|
||||
|
||||
$correlation_id = $GLOBALS['correlation_id'];
|
||||
error_log($correlation_id . ': Bulk print attendance list request started.');
|
||||
|
||||
require_once 'WorkflowEngine.php';
|
||||
require_once 'lib/tfpdf/font/unifont/ttfonts.php';
|
||||
define('FPDF_FONTPATH', 'lib/tfpdf/font/');
|
||||
require_once 'lib/tfpdf/tfpdf.php';
|
||||
|
||||
// Authentication check
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
if (ob_get_length()) ob_end_clean();
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
$error_message = 'Brak uprawnień.';
|
||||
error_log($correlation_id . ': ' . $error_message);
|
||||
echo json_encode(['error' => ['message' => $error_message], 'correlation_id' => $correlation_id]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Input validation
|
||||
$person_ids = json_decode($_POST['person_ids'] ?? '[]', true);
|
||||
error_log($correlation_id . ': Received ' . count($person_ids) . ' person_ids.');
|
||||
|
||||
if (empty($person_ids)) {
|
||||
if (ob_get_length()) ob_end_clean();
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
$error_message = 'Nie wybrano żadnych osób.';
|
||||
error_log($correlation_id . ': ' . $error_message);
|
||||
echo json_encode(['error' => ['message' => $error_message], 'correlation_id' => $correlation_id]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$workflowEngine = new WorkflowEngine();
|
||||
$peopleDetails = $workflowEngine->getPeopleDetails($person_ids);
|
||||
error_log($correlation_id . ': Fetched ' . count($peopleDetails) . ' rows from database.');
|
||||
|
||||
class PDF extends tFPDF
|
||||
{
|
||||
function __construct($orientation = 'P', $unit = 'mm', $size = 'A4')
|
||||
{
|
||||
parent::__construct($orientation, $unit, $size);
|
||||
$this->AddFont('DejaVu', '', 'DejaVuSans.ttf', true);
|
||||
$this->AddFont('DejaVu', 'B', 'DejaVuSans-Bold.ttf', true);
|
||||
$this->SetFont('DejaVu', '', 14);
|
||||
}
|
||||
|
||||
function generateAttendanceList($people)
|
||||
{
|
||||
$this->AddPage();
|
||||
|
||||
// Title
|
||||
$this->SetFont('DejaVu', 'B', 20);
|
||||
$this->Cell(0, 10, 'Lista obecności', 0, 1, 'C');
|
||||
$this->Ln(2);
|
||||
|
||||
// Subtitle - Date
|
||||
$this->SetFont('DejaVu', '', 10);
|
||||
$this->Cell(0, 10, date('Y-m-d H:i'), 0, 1, 'C');
|
||||
$this->Ln(10);
|
||||
|
||||
// Table Header
|
||||
$this->SetFont('DejaVu', 'B', 12);
|
||||
$this->SetFillColor(240, 240, 240);
|
||||
$this->Cell($this->GetPageWidth() * 0.7, 10, 'Imię i nazwisko', 1, 0, 'L', true);
|
||||
$this->Cell($this->GetPageWidth() * 0.3 - $this->lMargin - $this->rMargin, 10, 'Podpis', 1, 1, 'L', true);
|
||||
|
||||
// Table Body
|
||||
$this->SetFont('DejaVu', '', 12);
|
||||
foreach ($people as $person) {
|
||||
$name = $person['first_name'] . ' ' . $person['last_name'];
|
||||
$this->Cell($this->GetPageWidth() * 0.7, 15, $name, 1, 0, 'L');
|
||||
$this->Cell($this->GetPageWidth() * 0.3 - $this->lMargin - $this->rMargin, 15, '', 1, 1, 'L');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pdf = new PDF();
|
||||
$pdf->generateAttendanceList($peopleDetails);
|
||||
|
||||
$pdfData = $pdf->Output('S');
|
||||
error_log($correlation_id . ': PDF data generated. Length: ' . strlen($pdfData) . ' bytes.');
|
||||
|
||||
if (empty($pdfData)) {
|
||||
if (ob_get_length()) ob_end_clean();
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json');
|
||||
$error_message = 'Failed to generate PDF data.';
|
||||
error_log($correlation_id . ': ' . $error_message);
|
||||
echo json_encode(['error' => ['message' => $error_message], 'correlation_id' => $correlation_id]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (ob_get_length()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: attachment; filename="lista-obecnosci.pdf"');
|
||||
header('Content-Transfer-Encoding: binary');
|
||||
header('Cache-Control: private, max-age=0, must-revalidate');
|
||||
header('Pragma: public');
|
||||
header('Content-Length: ' . strlen($pdfData));
|
||||
|
||||
echo $pdfData;
|
||||
exit;
|
||||
109
_bulk_print_badges.php
Normal file
109
_bulk_print_badges.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
require_once 'lib/ErrorHandler.php';
|
||||
register_error_handler();
|
||||
|
||||
session_start();
|
||||
|
||||
$correlation_id = $GLOBALS['correlation_id']; // Use correlation_id from the global error handler
|
||||
error_log($correlation_id . ': Bulk print badges request started.');
|
||||
|
||||
require_once 'WorkflowEngine.php';
|
||||
require_once 'lib/tfpdf/font/unifont/ttfonts.php';
|
||||
define('FPDF_FONTPATH', 'lib/tfpdf/font/');
|
||||
require_once 'lib/tfpdf/tfpdf.php';
|
||||
|
||||
// Authentication check
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
if (ob_get_length()) ob_end_clean();
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
$error_message = 'Brak uprawnień.';
|
||||
error_log($correlation_id . ': ' . $error_message);
|
||||
echo json_encode(['error' => ['message' => $error_message], 'correlation_id' => $correlation_id]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Input validation
|
||||
$person_ids = json_decode($_POST['person_ids'] ?? '[]', true);
|
||||
error_log($correlation_id . ': Received ' . count($person_ids) . ' person_ids.');
|
||||
|
||||
if (empty($person_ids)) {
|
||||
if (ob_get_length()) ob_end_clean();
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
$error_message = 'Nie wybrano żadnych osób.';
|
||||
error_log($correlation_id . ': ' . $error_message);
|
||||
echo json_encode(['error' => ['message' => $error_message], 'correlation_id' => $correlation_id]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$workflowEngine = new WorkflowEngine();
|
||||
$peopleDetails = $workflowEngine->getPeopleDetails($person_ids);
|
||||
error_log($correlation_id . ': Fetched ' . count($peopleDetails) . ' rows from database.');
|
||||
|
||||
class PDF extends tFPDF
|
||||
{
|
||||
function __construct($orientation='P', $unit='mm', $size='A4')
|
||||
{
|
||||
parent::__construct($orientation, $unit, $size);
|
||||
$this->AddFont('DejaVu','','DejaVuSans.ttf',true);
|
||||
$this->AddFont('DejaVu','B','DejaVuSans-Bold.ttf',true);
|
||||
$this->SetFont('DejaVu', '', 14);
|
||||
}
|
||||
|
||||
function generateBadge($person) {
|
||||
$this->AddPage();
|
||||
$this->Image('assets/pasted-20260112-081646-4e946aad.png', 0, 0, $this->GetPageWidth(), $this->GetPageHeight());
|
||||
$this->SetY(20);
|
||||
$this->SetFont('DejaVu', 'B', 12);
|
||||
$this->Cell(0, 6, $person['first_name'] . ' ' . $person['last_name'], 0, 1, 'C');
|
||||
$this->SetFont('DejaVu', 'B', 8);
|
||||
$this->Cell(0, 3, $person['company_name'] ?? 'N/A', 0, 1, 'C');
|
||||
$this->SetFont('DejaVu', '', 6);
|
||||
$this->Cell(0, 3, $person['industry'] ?? 'N/A', 0, 1, 'C');
|
||||
|
||||
$this->SetXY(2.5,13);
|
||||
$this->SetFont('DejaVu', 'B', 6);
|
||||
$this->Cell(22, 3, $person['bni_group_name'] ?? 'GOŚĆ', 0, 0, 'C');
|
||||
}
|
||||
}
|
||||
|
||||
$pdf = new PDF('L', 'mm', array(85, 55));
|
||||
|
||||
foreach ($peopleDetails as $person) {
|
||||
// No need for converting the entire array, FPDF with iconv handles it.
|
||||
$pdf->generateBadge($person);
|
||||
}
|
||||
|
||||
// 2. Generate PDF content as a string
|
||||
$pdfData = $pdf->Output('S');
|
||||
error_log($correlation_id . ': PDF data generated. Length: ' . strlen($pdfData) . ' bytes.');
|
||||
error_log($correlation_id . ': Memory usage: ' . memory_get_usage());
|
||||
|
||||
// 3. Validate PDF data
|
||||
if (empty($pdfData) || !is_string($pdfData)) {
|
||||
if (ob_get_length()) ob_end_clean();
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json');
|
||||
$error_message = 'Failed to generate PDF data.';
|
||||
error_log($correlation_id . ': ' . $error_message);
|
||||
echo json_encode(['error' => ['message' => $error_message], 'correlation_id' => $correlation_id]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 4. Clean any potential output that occurred before this point
|
||||
if (ob_get_length()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
// 5. Send correct headers
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: attachment; filename="badges.pdf"');
|
||||
header('Content-Transfer-Encoding: binary');
|
||||
header('Cache-Control: private, max-age=0, must-revalidate');
|
||||
header('Pragma: public');
|
||||
header('Content-Length: ' . strlen($pdfData));
|
||||
|
||||
// 6. Output the raw PDF data and terminate
|
||||
echo $pdfData;
|
||||
exit;
|
||||
52
_bulk_update_status.php
Normal file
52
_bulk_update_status.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
require_once 'lib/ErrorHandler.php';
|
||||
require_once 'WorkflowEngine.php';
|
||||
require_once 'db/config.php';
|
||||
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
throw new WorkflowNotAllowedException('Unauthorized');
|
||||
}
|
||||
|
||||
$person_ids = json_decode($_POST['person_ids'] ?? '[]');
|
||||
$process_id = $_POST['process_id'] ?? null;
|
||||
$status = $_POST['status'] ?? null;
|
||||
$reason = $_POST['reason'] ?? '';
|
||||
$userId = $_SESSION['user_id'];
|
||||
|
||||
if (empty($person_ids) || !$process_id || !$status) {
|
||||
throw new WorkflowRuleFailedException('Missing parameters: person_ids, process_id, and status are required.');
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
$placeholders = implode(',', array_fill(0, count($person_ids), '?'));
|
||||
$stmt = $pdo->prepare("SELECT id FROM process_instances WHERE process_definition_id = ? AND person_id IN ($placeholders)");
|
||||
$params = array_merge([$process_id], $person_ids);
|
||||
$stmt->execute($params);
|
||||
$instance_ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (empty($instance_ids)) {
|
||||
$_SESSION['flash_message'] = "No instances found for the selected people and process.";
|
||||
header('Location: ' . $_SERVER['HTTP_REFERER']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$statuses = [];
|
||||
foreach ($instance_ids as $instance_id) {
|
||||
$statuses[] = [
|
||||
'instance_id' => $instance_id,
|
||||
'status' => $status,
|
||||
'reason' => $reason,
|
||||
'user_id' => $userId
|
||||
];
|
||||
}
|
||||
|
||||
$workflowEngine = new WorkflowEngine();
|
||||
$results = $workflowEngine->bulkManualStatus($statuses);
|
||||
|
||||
$_SESSION['flash_message'] = "Bulk status update completed.";
|
||||
$_SESSION['bulk_results'] = $results;
|
||||
|
||||
header('Location: ' . $_SERVER['HTTP_REFERER']);
|
||||
exit;
|
||||
119
_create_person.php
Normal file
119
_create_person.php
Normal file
@ -0,0 +1,119 @@
|
||||
<?php
|
||||
require_once 'lib/ErrorHandler.php';
|
||||
require_once 'db/config.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => ['message' => 'Method not allowed.'], 'correlation_id' => uniqid()]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$first_name = $_POST['first_name'] ?? '';
|
||||
$last_name = $_POST['last_name'] ?? '';
|
||||
$email = $_POST['email'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
$company_name = $_POST['company_name'] ?? null;
|
||||
$phone = $_POST['phone'] ?? null;
|
||||
$role = $_POST['role'] ?? 'guest';
|
||||
$functions = isset($_POST['functions']) ? (array)$_POST['functions'] : [];
|
||||
$bni_group_id = isset($_POST['bni_group_id']) && !empty($_POST['bni_group_id']) ? $_POST['bni_group_id'] : null;
|
||||
|
||||
$nip = $_POST['nip'] ?? null;
|
||||
$industry = $_POST['industry'] ?? null;
|
||||
$company_size_revenue = $_POST['company_size_revenue'] ?? null;
|
||||
$business_description = $_POST['business_description'] ?? null;
|
||||
|
||||
if (empty($first_name) || empty($last_name) || empty($email) || empty($password)) {
|
||||
http_response_code(422);
|
||||
echo json_encode(['error' => ['message' => 'First name, last name, email, and password are required.'], 'correlation_id' => uniqid()]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($role !== 'member') {
|
||||
$bni_group_id = null;
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$sql = 'INSERT INTO people (first_name, last_name, email, password, company_name, phone, role, bni_group_id, nip, industry, company_size_revenue, business_description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$first_name, $last_name, $email, password_hash($password, PASSWORD_DEFAULT), $company_name, $phone, $role, $bni_group_id, $nip, $industry, $company_size_revenue, $business_description]);
|
||||
$personId = $pdo->lastInsertId();
|
||||
|
||||
$upload_dir = 'uploads/people/' . $personId . '/';
|
||||
if (!is_dir($upload_dir)) {
|
||||
if (!mkdir($upload_dir, 0777, true) && !is_dir($upload_dir)) {
|
||||
throw new RuntimeException(sprintf('Directory "%s" was not created', $upload_dir));
|
||||
}
|
||||
}
|
||||
|
||||
$file_fields = [
|
||||
'company_logo' => 'company_logo_path',
|
||||
'person_photo' => 'person_photo_path',
|
||||
'gains_sheet' => 'gains_sheet_path',
|
||||
'top_wanted_contacts' => 'top_wanted_contacts_path',
|
||||
'top_owned_contacts' => 'top_owned_contacts_path'
|
||||
];
|
||||
$file_paths_to_update = [];
|
||||
|
||||
foreach ($file_fields as $form_field_name => $db_column_name) {
|
||||
if (isset($_FILES[$form_field_name]) && $_FILES[$form_field_name]['error'] == UPLOAD_ERR_OK) {
|
||||
$tmp_name = $_FILES[$form_field_name]['tmp_name'];
|
||||
$original_name = basename($_FILES[$form_field_name]['name']);
|
||||
$file_ext = pathinfo($original_name, PATHINFO_EXTENSION);
|
||||
$new_filename = uniqid($form_field_name . '_', true) . '.' . $file_ext;
|
||||
$destination = $upload_dir . $new_filename;
|
||||
|
||||
if (move_uploaded_file($tmp_name, $destination)) {
|
||||
$file_paths_to_update[$db_column_name] = $destination;
|
||||
} else {
|
||||
throw new RuntimeException("Failed to move uploaded file for {$form_field_name}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($file_paths_to_update)) {
|
||||
$sql_parts = [];
|
||||
$params = [];
|
||||
foreach ($file_paths_to_update as $column => $path) {
|
||||
$sql_parts[] = "$column = ?";
|
||||
$params[] = $path;
|
||||
}
|
||||
$params[] = $personId;
|
||||
$sql = "UPDATE people SET " . implode(', ', $sql_parts) . " WHERE id = ?";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
}
|
||||
|
||||
if (!empty($functions)) {
|
||||
$sql = "INSERT INTO user_functions (user_id, function_id) VALUES (?, ?)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
foreach ($functions as $functionId) {
|
||||
$stmt->execute([$personId, $functionId]);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode(['success' => true, 'person_id' => $personId, 'message' => 'Person created successfully.']);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
if ($e->errorInfo[1] == 1062) {
|
||||
http_response_code(409); // Conflict
|
||||
echo json_encode(['error' => ['message' => 'An account with this email address already exists.'], 'correlation_id' => uniqid()]);
|
||||
} else {
|
||||
throw $e; // Re-throw to be caught by the global error handler
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $e; // Re-throw to be caught by the global error handler
|
||||
}
|
||||
20
_delete_bni_group.php
Normal file
20
_delete_bni_group.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
if (isset($_GET['id'])) {
|
||||
$id = $_GET['id'];
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("DELETE FROM bni_groups WHERE id = :id");
|
||||
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
$_SESSION['success_message'] = 'BNI Group deleted successfully!';
|
||||
} catch (PDOException $e) {
|
||||
$_SESSION['error_message'] = 'Error deleting BNI group: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: bni_groups.php');
|
||||
exit;
|
||||
48
_delete_calendar_event.php
Normal file
48
_delete_calendar_event.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header("Location: login.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once 'db/config.php';
|
||||
|
||||
if (isset($_GET['id'])) {
|
||||
$event_id = $_GET['id'];
|
||||
$pdo = db();
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Check if the event is a parent event
|
||||
$stmt = $pdo->prepare("SELECT parent_event_id FROM calendar_events WHERE id = ?");
|
||||
$stmt->execute([$event_id]);
|
||||
$event = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($event) {
|
||||
if ($event['parent_event_id'] === null) {
|
||||
// It's a parent event, delete it and all its children
|
||||
$stmt_delete_children = $pdo->prepare("DELETE FROM calendar_events WHERE parent_event_id = ?");
|
||||
$stmt_delete_children->execute([$event_id]);
|
||||
}
|
||||
|
||||
// Delete the event itself
|
||||
$stmt_delete = $pdo->prepare("DELETE FROM calendar_events WHERE id = ?");
|
||||
$stmt_delete->execute([$event_id]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
header("Location: calendar.php");
|
||||
exit();
|
||||
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
error_log($e->getMessage());
|
||||
header("Location: calendar.php?error=db_error");
|
||||
exit();
|
||||
}
|
||||
} else {
|
||||
header("Location: calendar.php");
|
||||
exit();
|
||||
}
|
||||
16
_delete_event_type.php
Normal file
16
_delete_event_type.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
|
||||
if (isset($_GET['id'])) {
|
||||
$id = $_GET['id'];
|
||||
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("DELETE FROM event_types WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
session_start();
|
||||
$_SESSION['success_message'] = 'Event type deleted successfully.';
|
||||
header('Location: event_types.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
21
_delete_function.php
Normal file
21
_delete_function.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['user_id']) || !isset($_GET['id'])) {
|
||||
header('Location: login.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
include 'db/config.php';
|
||||
|
||||
$id = $_GET['id'];
|
||||
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("DELETE FROM functions WHERE id = :id");
|
||||
$stmt->execute(['id' => $id]);
|
||||
|
||||
// Optional: Also delete user_functions associated with this function
|
||||
$stmt = $pdo->prepare("DELETE FROM user_functions WHERE function_id = :function_id");
|
||||
$stmt->execute(['function_id' => $id]);
|
||||
|
||||
header('Location: functions.php');
|
||||
exit();
|
||||
51
_delete_person.php
Normal file
51
_delete_person.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
session_start();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['person_id'])) {
|
||||
$id = $_POST['person_id'];
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("DELETE FROM people WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
echo json_encode(['success' => true, 'message' => 'Osoba usunięta pomyślnie.']);
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'error' => 'Nie znaleziono osoby.']);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
// Log the real error to a secure log file
|
||||
error_log("Database error on person delete: " . $e->getMessage());
|
||||
// Send a generic error message to the client
|
||||
echo json_encode(['success' => false, 'error' => 'Błąd serwera podczas usuwania osoby.']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Brak ID osoby.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Keep GET for backwards compatibility or simple cases, but it redirects.
|
||||
if (isset($_GET['id'])) {
|
||||
$id = $_GET['id'];
|
||||
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("DELETE FROM people WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
$_SESSION['success_message'] = 'Osoba usunięta pomyślnie.';
|
||||
header('Location: persons.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'error' => 'Nieprawidłowa metoda żądania.']);
|
||||
?>
|
||||
28
_footer.php
Normal file
28
_footer.php
Normal file
@ -0,0 +1,28 @@
|
||||
<!-- Bootstrap Bundle with Popper -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<!-- jQuery UI -->
|
||||
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"></script>
|
||||
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css">
|
||||
|
||||
<!-- Custom JS -->
|
||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const sidebarLinks = document.querySelectorAll('#sidebar .nav-link');
|
||||
const currentPath = window.location.pathname.split('/').pop();
|
||||
|
||||
sidebarLinks.forEach(link => {
|
||||
const linkPath = link.getAttribute('href').split('/').pop();
|
||||
if (linkPath === currentPath) {
|
||||
link.classList.add('active');
|
||||
} else {
|
||||
link.classList.remove('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
40
_get_event_details.php
Normal file
40
_get_event_details.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header("HTTP/1.1 401 Unauthorized");
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once 'db/config.php';
|
||||
|
||||
if (isset($_GET['id'])) {
|
||||
$event_id = $_GET['id'];
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT c.*, t.name as type_name, GROUP_CONCAT(g.id) as group_ids
|
||||
FROM calendar_events c
|
||||
LEFT JOIN event_types t ON c.event_type_id = t.id
|
||||
LEFT JOIN calendar_event_groups ceg ON c.id = ceg.calendar_event_id
|
||||
LEFT JOIN bni_groups g ON ceg.bni_group_id = g.id
|
||||
WHERE c.id = ?
|
||||
GROUP BY c.id
|
||||
");
|
||||
$stmt->execute([$event_id]);
|
||||
$event = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($event) {
|
||||
if ($event['group_ids']) {
|
||||
$event['group_ids'] = explode(',', $event['group_ids']);
|
||||
} else {
|
||||
$event['group_ids'] = [];
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($event);
|
||||
} else {
|
||||
header("HTTP/1.1 404 Not Found");
|
||||
}
|
||||
} else {
|
||||
header("HTTP/1.1 400 Bad Request");
|
||||
}
|
||||
41
_get_future_meetings.php
Normal file
41
_get_future_meetings.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
$bni_group_id = isset($_GET['bni_group_id']) ? (int)$_GET['bni_group_id'] : 0;
|
||||
$offset = isset($_GET['offset']) ? (int)$_GET['offset'] : 0;
|
||||
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 2;
|
||||
|
||||
if ($bni_group_id === 0) {
|
||||
echo json_encode(['error' => 'Invalid BNI Group ID']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
$sql = "
|
||||
SELECT ce.*, bg.name as group_name
|
||||
FROM calendar_events ce
|
||||
JOIN calendar_event_groups ceg ON ce.id = ceg.calendar_event_id
|
||||
JOIN bni_groups bg ON ceg.bni_group_id = bg.id
|
||||
WHERE ceg.bni_group_id = :bni_group_id
|
||||
AND ce.start_datetime > NOW()
|
||||
ORDER BY ce.start_datetime ASC
|
||||
LIMIT :limit OFFSET :offset
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindParam(':bni_group_id', $bni_group_id, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
$events = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode($events);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
|
||||
}
|
||||
266
_get_instance_details.php
Normal file
266
_get_instance_details.php
Normal file
@ -0,0 +1,266 @@
|
||||
<?php
|
||||
require_once 'WorkflowEngine.php';
|
||||
require_once 'lib/ErrorHandler.php';
|
||||
require_once 'lib/WorkflowExceptions.php';
|
||||
|
||||
session_start();
|
||||
|
||||
// Security check
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$person_id = $_GET['person_id'] ?? null;
|
||||
$process_definition_id = $_GET['process_id'] ?? null;
|
||||
|
||||
if (!$person_id || !$process_definition_id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Missing person_id or process_id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$userId = $_SESSION['user_id'];
|
||||
$engine = new WorkflowEngine();
|
||||
$pdo = db();
|
||||
|
||||
// Fetch Person and Process Definition details first
|
||||
$stmt_person = $pdo->prepare("SELECT first_name, last_name FROM people WHERE id = ?");
|
||||
$stmt_person->execute([$person_id]);
|
||||
$person = $stmt_person->fetch();
|
||||
|
||||
$stmt_process = $pdo->prepare("SELECT * FROM process_definitions WHERE id = ?");
|
||||
$stmt_process->execute([$process_definition_id]);
|
||||
$process = $stmt_process->fetch();
|
||||
|
||||
if (!$person || !$process) {
|
||||
http_response_code(404);
|
||||
echo "<p class='text-danger'>Could not find person or process.</p>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// Try to find an existing instance
|
||||
$instance = $engine->getInstanceByDefId($person_id, $process_definition_id);
|
||||
|
||||
?>
|
||||
|
||||
<!-- Title for the modal, to be grabbed by JS -->
|
||||
<div id="instance-modal-title" class="d-none">
|
||||
<?= htmlspecialchars($person['first_name']." ".$person['last_name']) ?> - <?= htmlspecialchars($process['name']) ?>
|
||||
</div>
|
||||
|
||||
<?php if ($instance): // INSTANCE EXISTS ?>
|
||||
<?php
|
||||
$instanceId = $instance['id'];
|
||||
$definition = $process['definition_json'] ? json_decode($process['definition_json'], true) : null;
|
||||
$isChecklist = ($definition && isset($definition['type']) && $definition['type'] === 'checklist');
|
||||
$events = $engine->getEvents($instanceId);
|
||||
?>
|
||||
|
||||
<?php if ($isChecklist): ?>
|
||||
<?php
|
||||
$tasks = $definition['tasks'] ?? [];
|
||||
$instanceData = $instance['data_json'] ? json_decode($instance['data_json'], true) : [];
|
||||
?>
|
||||
<div class="checklist-modal-container" data-instance-id="<?= $instanceId ?>">
|
||||
<h5>Zadania do wykonania</h5>
|
||||
<div class="checklist-container">
|
||||
<?php foreach ($tasks as $task):
|
||||
$isChecked = !empty($instanceData[$task['code']]);
|
||||
?>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input task-checkbox-modal" type="checkbox" value=""
|
||||
data-task-code="<?= $task['code'] ?>" <?= $isChecked ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" title="<?= htmlspecialchars($task['name']) ?>">
|
||||
<?= htmlspecialchars($task['name']) ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$currentNodeId = $instance['current_node_id'];
|
||||
$all_nodes = $engine->getProcessDefinitionNodes($process_definition_id);
|
||||
$availableTransitions = $engine->getAvailableTransitions($instanceId);
|
||||
|
||||
$available_target_node_ids = array_map(function($t) { return $t['to']; }, $availableTransitions);
|
||||
$available_transitions_map = [];
|
||||
foreach ($availableTransitions as $t) {
|
||||
$available_transitions_map[$t['id']] = $t;
|
||||
}
|
||||
|
||||
$visited_nodes = [];
|
||||
foreach ($events as $event) {
|
||||
if ($event['node_id']) {
|
||||
$visited_nodes[$event['node_id']] = true;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="process-steps-container">
|
||||
<h5>Kroki procesu</h5>
|
||||
<ul class="list-group">
|
||||
<?php foreach ($all_nodes as $nodeId => $node):
|
||||
if (!isset($node['ui_hints']['title']) || $node['ui_hints']['title'] === '') continue;
|
||||
$is_current = ($currentNodeId === $nodeId);
|
||||
$is_completed = isset($visited_nodes[$nodeId]) && !$is_current;
|
||||
|
||||
$status_icon = '';
|
||||
$li_class = '';
|
||||
|
||||
if ($is_current) {
|
||||
$li_class = 'list-group-item-primary';
|
||||
$status_icon = '<i class="bi bi-arrow-right-circle-fill text-primary me-2"></i>';
|
||||
} elseif ($is_completed) {
|
||||
$li_class = 'list-group-item-success';
|
||||
$status_icon = '<i class="bi bi-check-circle-fill text-success me-2"></i>';
|
||||
} else {
|
||||
$li_class = 'text-muted';
|
||||
$status_icon = '<i class="bi bi-circle me-2"></i>';
|
||||
}
|
||||
?>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center <?= $li_class ?>">
|
||||
<div>
|
||||
<?= $status_icon ?>
|
||||
<strong><?= htmlspecialchars($node['ui_hints']['title']) ?></strong>
|
||||
</div>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<div class="mt-3">
|
||||
<h5>Available Actions</h5>
|
||||
<?php
|
||||
$currentNode = $all_nodes[$currentNodeId] ?? null;
|
||||
if ($currentNode && isset($currentNode['ui_hints']['form_schema'])):
|
||||
?>
|
||||
<form id="transition-form">
|
||||
<?php foreach ($currentNode['ui_hints']['form_schema'] as $field): ?>
|
||||
<div class="mb-3">
|
||||
<label for="<?= $field['name'] ?>" class="form-label"><?= $field['label'] ?></label>
|
||||
<?php if ($field['type'] === '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: ?>
|
||||
<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; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php if (empty($availableTransitions)): ?>
|
||||
<p>No actions available.</p>
|
||||
<?php else: ?>
|
||||
<?php foreach ($availableTransitions as $transition): ?>
|
||||
<button class="btn btn-sm btn-primary apply-transition-btn"
|
||||
data-instance-id="<?= $instanceId ?>"
|
||||
data-transition-id="<?= $transition['id'] ?>">
|
||||
<?= htmlspecialchars($transition['name']) ?>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<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="<?= $instanceId ?>">Dodaj notatkę</button>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="history-container">
|
||||
<h5>Historia</h5>
|
||||
<?php if (empty($events)): ?>
|
||||
<p>Brak zdarzeń.</p>
|
||||
<?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">
|
||||
<?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">
|
||||
<?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>
|
||||
<?php
|
||||
if (!empty($event['message'])) {
|
||||
$message = $payload['message'] ?? $event['message'];
|
||||
echo '<p class="mb-1 text-muted fst-italic">' . htmlspecialchars($message) . '</p>';
|
||||
}
|
||||
?>
|
||||
<?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>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php else: // NO INSTANCE EXISTS ?>
|
||||
<?php
|
||||
$eligibility = $engine->checkEligibility($person_id, $process_definition_id);
|
||||
?>
|
||||
|
||||
<div class="text-center">
|
||||
<?php if ($eligibility['is_eligible']): ?>
|
||||
<h4>Process Not Started</h4>
|
||||
<p>This process has not been started for this person.</p>
|
||||
<button id="startProcessBtn" class="btn btn-primary" data-person-id="<?= $person_id ?>" data-process-id="<?= $process_definition_id ?>">
|
||||
Start Process
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<h4>Not Eligible</h4>
|
||||
<p>This person is not eligible to start this process.</p>
|
||||
<ul class="list-group list-group-flush text-start">
|
||||
<?php foreach ($eligibility['reasons'] as $reason): ?>
|
||||
<li class="list-group-item list-group-item-danger"><?= htmlspecialchars($reason) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
35
_get_meeting_attendance.php
Normal file
35
_get_meeting_attendance.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
require_once 'WorkflowEngine.php';
|
||||
|
||||
session_start();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$response = ['success' => false, 'message' => 'An error occurred.', 'attendance' => []];
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
$response['message'] = 'You must be logged in to perform this action.';
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
$groupId = $_GET['group_id'] ?? null;
|
||||
$meetingDate = $_GET['meeting_date'] ?? null;
|
||||
|
||||
if (!$groupId || !$meetingDate) {
|
||||
$response['message'] = 'Missing required parameters.';
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$workflowEngine = new WorkflowEngine();
|
||||
$attendance = $workflowEngine->getMeetingAttendanceByGroupAndDate((int)$groupId, $meetingDate);
|
||||
$response['success'] = true;
|
||||
$response['attendance'] = $attendance;
|
||||
} catch (Exception $e) {
|
||||
error_log($e->getMessage());
|
||||
$response['message'] = 'Error fetching attendance: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
27
_get_meeting_details.php
Normal file
27
_get_meeting_details.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
require_once 'WorkflowEngine.php';
|
||||
|
||||
session_start();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$response = ['success' => false, 'message' => 'Invalid request'];
|
||||
|
||||
$personId = $_GET['person_id'] ?? null;
|
||||
$bniGroupId = $_GET['bni_group_id'] ?? null;
|
||||
$meetingDatetime = $_GET['meeting_datetime'] ?? null;
|
||||
$userId = $_SESSION['user_id'] ?? 0; // Ensure you have a user ID in the session
|
||||
|
||||
if ($personId && $bniGroupId && $meetingDatetime && $userId) {
|
||||
try {
|
||||
$workflowEngine = new WorkflowEngine();
|
||||
$details = $workflowEngine->getMeetingDetails((int)$personId, (int)$bniGroupId, $meetingDatetime);
|
||||
$response = ['success' => true, 'details' => $details];
|
||||
} catch (Exception $e) {
|
||||
$response['message'] = $e->getMessage();
|
||||
}
|
||||
} else {
|
||||
$response['message'] = 'Missing required parameters.';
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
66
_get_person_details.php
Normal file
66
_get_person_details.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
|
||||
if (isset($_GET['id'])) {
|
||||
header('Content-Type: application/json');
|
||||
try {
|
||||
$person_id = $_GET['id'];
|
||||
$pdo = db();
|
||||
|
||||
// Fetch person details
|
||||
$stmt = $pdo->prepare("SELECT * FROM people WHERE id = ?");
|
||||
$stmt->execute([$person_id]);
|
||||
$person = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// Fetch all functions
|
||||
$stmt = $pdo->query("
|
||||
SELECT f.id, f.name, bg.name as group_name
|
||||
FROM functions f
|
||||
LEFT JOIN bni_groups bg ON f.bni_group_id = bg.id
|
||||
ORDER BY bg.display_order, f.display_order
|
||||
");
|
||||
$all_functions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Fetch person's functions
|
||||
$stmt = $pdo->prepare("SELECT function_id FROM user_functions WHERE user_id = ?");
|
||||
$stmt->execute([$person_id]);
|
||||
$person_functions = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
|
||||
|
||||
// --- Fetch Follow-up Process Summary ---
|
||||
$follow_up_summary = null;
|
||||
$stmt_def = $pdo->prepare("SELECT id FROM process_definitions WHERE code = 'guest_handling' LIMIT 1");
|
||||
$stmt_def->execute();
|
||||
$follow_up_def_id = $stmt_def->fetchColumn();
|
||||
|
||||
if ($follow_up_def_id) {
|
||||
$stmt_inst = $pdo->prepare("SELECT * FROM process_instances WHERE person_id = ? AND process_definition_id = ? ORDER BY id DESC LIMIT 1");
|
||||
$stmt_inst->execute([$person_id, $follow_up_def_id]);
|
||||
$instance = $stmt_inst->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($instance) {
|
||||
$data = $instance['data_json'] ? json_decode($instance['data_json'], true) : [];
|
||||
$follow_up_summary = [
|
||||
'last_call_outcome' => $data['outcome_status'] ?? null,
|
||||
'last_call_date' => $data['call_date'] ?? null,
|
||||
'next_contact_date' => $data['next_contact_date'] ?? null,
|
||||
'final_outcome' => $instance['current_status'], // e.g., completed, terminated
|
||||
'reason' => $instance['current_reason']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$response = [
|
||||
'person' => $person,
|
||||
'all_functions' => $all_functions,
|
||||
'person_functions' => $person_functions,
|
||||
'follow_up_summary' => $follow_up_summary
|
||||
];
|
||||
|
||||
echo json_encode($response);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
96
_get_process_bulk_details.php
Normal file
96
_get_process_bulk_details.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isset($_GET['process_id'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Process ID is required.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$process_id = $_GET['process_id'];
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
// 1. Get process definition details
|
||||
$stmt_def = $pdo->prepare("SELECT * FROM process_definitions WHERE id = ?");
|
||||
$stmt_def->execute([$process_id]);
|
||||
$process_definition = $stmt_def->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$process_definition) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Process definition not found.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!empty($process_definition['definition_json'])) {
|
||||
$process_definition['definition_json'] = json_decode($process_definition['definition_json'], true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception("Failed to decode process definition JSON. Error: " . json_last_error_msg());
|
||||
}
|
||||
} else {
|
||||
$process_definition['definition_json'] = [];
|
||||
}
|
||||
|
||||
// 2. Get all instances for this process
|
||||
$stmt_instances = $pdo->prepare("SELECT * FROM process_instances WHERE process_definition_id = ?");
|
||||
$stmt_instances->execute([$process_id]);
|
||||
$instances = $stmt_instances->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$instance_ids = array_map(function($i) { return $i['id']; }, $instances);
|
||||
|
||||
// 3. Get all events for these instances
|
||||
$events = [];
|
||||
if (!empty($instance_ids)) {
|
||||
$placeholders = implode(',', array_fill(0, count($instance_ids), '?'));
|
||||
$stmt_events = $pdo->prepare("SELECT * FROM process_events WHERE process_instance_id IN ($placeholders) ORDER BY created_at, id");
|
||||
$stmt_events->execute($instance_ids);
|
||||
$all_events = $stmt_events->fetchAll(PDO::FETCH_ASSOC);
|
||||
// Group events by instance_id
|
||||
foreach ($all_events as $event) {
|
||||
$events[$event['process_instance_id']][] = $event;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Get People details
|
||||
$people_ids = array_unique(array_column($instances, 'person_id'));
|
||||
$people = [];
|
||||
if (!empty($people_ids)) {
|
||||
$valid_people_ids = array_filter($people_ids, 'is_numeric');
|
||||
|
||||
if (!empty($valid_people_ids)) {
|
||||
$placeholders = implode(',', array_fill(0, count($valid_people_ids), '?'));
|
||||
$stmt_people = $pdo->prepare("SELECT id, first_name, last_name FROM people WHERE id IN ($placeholders)");
|
||||
$stmt_people->execute(array_values($valid_people_ids));
|
||||
$people_results = $stmt_people->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($people_results as $person) {
|
||||
$people[$person['id']] = $person;
|
||||
$people[$person['id']]['name'] = trim($person['first_name'] . ' ' . $person['last_name']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assemble the response
|
||||
// Ensure steps are available, even if the JSON is empty or malformed.
|
||||
$steps = !empty($process_definition['definition_json']['steps']) ? $process_definition['definition_json']['steps'] : [];
|
||||
|
||||
$response = [
|
||||
'process' => $process_definition,
|
||||
'steps' => $steps,
|
||||
'instances' => $instances,
|
||||
'events' => $events,
|
||||
'people' => $people
|
||||
];
|
||||
|
||||
echo json_encode($response);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'A database error occurred.', 'details' => $e->getMessage()]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'A general error occurred.', 'details' => $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
52
_header.php
Normal file
52
_header.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?php echo getenv('PROJECT_NAME') ?: 'BNI obsługa regionu'; ?> - Dashboard</title>
|
||||
<meta name="description" content="<?php echo getenv('PROJECT_DESCRIPTION') ?: 'A modern web application.'; ?>">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
|
||||
<!-- OG Meta Tags -->
|
||||
<meta property="og:title" content="<?php echo getenv('PROJECT_NAME') ?: 'BNI obsługa regionu'; ?>">
|
||||
<meta property="og:description" content="<?php echo getenv('PROJECT_DESCRIPTION') ?: 'A modern web application.'; ?>">
|
||||
<meta property="og:image" content="<?php echo getenv('PROJECT_IMAGE_URL') ?: 'https://via.placeholder.com/1200x630.png?text=Visit+My+App'; ?>">
|
||||
<meta property="og:url" content="<?php echo (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]"; ?>">
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="<?php echo getenv('PROJECT_NAME') ?: 'BNI obsługa regionu'; ?>">
|
||||
<meta name="twitter:description" content="<?php echo getenv('PROJECT_DESCRIPTION') ?: 'A modern web application.'; ?>">
|
||||
<meta name="twitter:image" content="<?php echo getenv('PROJECT_IMAGE_URL') ?: 'https://via.placeholder.com/1200x630.png?text=Visit+My+App'; ?>'>
|
||||
<link rel="icon" href="assets/pasted-20260111-144117-aba8ec29.jpg" type="image/jpeg">
|
||||
|
||||
</head>
|
||||
<body class="<?php echo isset($_COOKIE['sidebar_collapsed']) && $_COOKIE['sidebar_collapsed'] === 'true' ? 'sidebar-collapsed' : ''; ?>">
|
||||
<script>
|
||||
(function() {
|
||||
if (localStorage.getItem('sidebarCollapsed') === 'true') {
|
||||
document.body.classList.add('sidebar-collapsed');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
37
_init_instances.php
Normal file
37
_init_instances.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
|
||||
session_start();
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
|
||||
// Get all active people
|
||||
$stmt_people = $pdo->prepare("SELECT id FROM people WHERE active = 1");
|
||||
$stmt_people->execute();
|
||||
$people = $stmt_people->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
// Get all active process definitions
|
||||
$stmt_processes = $pdo->prepare("SELECT id FROM process_definitions WHERE is_active = 1");
|
||||
$stmt_processes->execute();
|
||||
$processes = $stmt_processes->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
$insert_stmt = $pdo->prepare("INSERT IGNORE INTO process_instances (person_id, process_definition_id, current_status) VALUES (?, ?, 'none')");
|
||||
|
||||
$count = 0;
|
||||
foreach ($people as $person_id) {
|
||||
foreach ($processes as $process_id) {
|
||||
$insert_stmt->execute([$person_id, $process_id]);
|
||||
if ($insert_stmt->rowCount() > 0) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$_SESSION['flash_message'] = "Initialized $count new process instances.";
|
||||
|
||||
header("Location: process_dashboard.php"); // Redirect to the main dashboard
|
||||
exit;
|
||||
52
_init_single_instance.php
Normal file
52
_init_single_instance.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
require_once 'lib/ErrorHandler.php';
|
||||
register_error_handler();
|
||||
|
||||
require_once 'db/config.php';
|
||||
require_once 'WorkflowEngine.php';
|
||||
|
||||
if (session_status() == PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => ['message' => 'Authentication required.']]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$userId = $_SESSION['user_id'];
|
||||
$personId = filter_input(INPUT_POST, 'person_id', FILTER_VALIDATE_INT);
|
||||
$processDefinitionId = filter_input(INPUT_POST, 'process_id', FILTER_VALIDATE_INT);
|
||||
$deleteExisting = filter_input(INPUT_POST, 'delete_existing');
|
||||
|
||||
if (!$personId || !$processDefinitionId) {
|
||||
// InvalidArgumentException will be caught by the handler and result in a 400 Bad Request
|
||||
throw new InvalidArgumentException('Invalid or missing person_id or process_id.');
|
||||
}
|
||||
|
||||
$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:
|
||||
// 1. Validating the process definition exists.
|
||||
// 2. Checking if the process is active.
|
||||
// 3. Checking if the person is eligible.
|
||||
// 4. Creating the instance if it doesn't exist.
|
||||
// It will throw specific exceptions (WorkflowNotFoundException, WorkflowNotAllowedException, WorkflowEligibilityException) which our ErrorHandler will turn into 404, 409, and 422 responses.
|
||||
$instance = $engine->getOrCreateInstanceByDefId($personId, $processDefinitionId, $userId);
|
||||
|
||||
if ($instance) {
|
||||
echo json_encode(['success' => true, 'message' => 'Process initialized successfully.', 'instance_id' => $instance['id']]);
|
||||
} else {
|
||||
// This case should not be reached if the engine works as expected, as failures should throw exceptions.
|
||||
throw new Exception("Failed to initialize process for an unknown reason.");
|
||||
}
|
||||
19
_navbar.php
Normal file
19
_navbar.php
Normal file
@ -0,0 +1,19 @@
|
||||
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
|
||||
<button class="navbar-toggler d-none d-md-block" type="button" id="sidebar-toggler" aria-label="Toggle sidebar" aria-expanded="true">
|
||||
<i class="bi bi-list"></i>
|
||||
</button>
|
||||
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="#"><img src="assets/pasted-20260111-143449-befa41d3.png" class="d-inline-block align-top navbar-logo" alt=""> <?php echo getenv('PROJECT_NAME') ?: 'BNI obsługa regionu'; ?></a>
|
||||
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<ul class="navbar-nav flex-row px-3">
|
||||
<?php if (isset($_SESSION['user_name'])): ?>
|
||||
<li class="nav-item text-nowrap">
|
||||
<span class="nav-link">Witaj, <?= htmlspecialchars($_SESSION['user_name']) ?></span>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<li class="nav-item text-nowrap ms-2">
|
||||
<a class="nav-link" href="logout.php">Wyloguj</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
97
_save_process_definition.php
Normal file
97
_save_process_definition.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
require_once 'lib/ErrorHandler.php';
|
||||
|
||||
register_error_handler();
|
||||
|
||||
session_start();
|
||||
|
||||
function validate_definition_json($json) {
|
||||
if (empty($json)) {
|
||||
return; // No validation for empty json
|
||||
}
|
||||
$data = json_decode($json, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
http_response_code(422);
|
||||
throw new WorkflowRuleFailedException('Invalid JSON format in definition.');
|
||||
}
|
||||
|
||||
$allowed_statuses = ['none', 'negative', 'in_progress', 'positive', 'active', 'processing', 'paused', 'completed', 'terminated'];
|
||||
|
||||
if (isset($data['nodes'])) {
|
||||
foreach ($data['nodes'] as $node) {
|
||||
if (isset($node['ui_hints']['status']) && !in_array($node['ui_hints']['status'], $allowed_statuses)) {
|
||||
http_response_code(422);
|
||||
throw new WorkflowRuleFailedException('Invalid status in ui_hints. Allowed values are: ' . implode(', ', $allowed_statuses));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['transitions'])) {
|
||||
foreach ($data['transitions'] as $transition) {
|
||||
if (isset($transition['actions'])) {
|
||||
foreach ($transition['actions'] as $action) {
|
||||
if ($action['type'] === 'start_process' && isset($action['process_name'])) {
|
||||
http_response_code(422);
|
||||
throw new WorkflowRuleFailedException('Use process_code instead of process_name in transition actions.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['eligibility_rules'])) {
|
||||
foreach ($data['eligibility_rules'] as $rule) {
|
||||
if ($rule['type'] === 'process_completed' && isset($rule['process_name'])) {
|
||||
http_response_code(422);
|
||||
throw new WorkflowRuleFailedException('Use process_code instead of process_name in eligibility_rules.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$processId = $_POST['process_id'] ?? null;
|
||||
$name = $_POST['name'] ?? '';
|
||||
$definition_json = $_POST['definition_json'] ?? '';
|
||||
|
||||
validate_definition_json($definition_json);
|
||||
|
||||
// Generate a simple code from the name
|
||||
$code = strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $name)));
|
||||
|
||||
if (empty($name)) {
|
||||
throw new WorkflowRuleFailedException('Process name is required.');
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
|
||||
if (empty($processId)) {
|
||||
// Create new process
|
||||
$sql = 'INSERT INTO process_definitions (name, code, definition_json, is_active) VALUES (?, ?, ?, 1)';
|
||||
$params = [$name, $code, $definition_json];
|
||||
$message = 'Process created successfully.';
|
||||
} else {
|
||||
// Update existing process
|
||||
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : 0;
|
||||
$sql = 'UPDATE process_definitions SET name = ?, definition_json = ?, is_active = ? WHERE id = ?';
|
||||
$params = [$name, $definition_json, $is_active, $processId];
|
||||
$message = 'Process updated successfully.';
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['message' => $message]);
|
||||
} else {
|
||||
$_SESSION['success_message'] = $message;
|
||||
header('Location: process_definitions.php');
|
||||
exit();
|
||||
}
|
||||
}
|
||||
} catch (WorkflowRuleFailedException $e) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
56
_sidebar.php
Normal file
56
_sidebar.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php $current_page = basename($_SERVER['PHP_SELF']); ?>
|
||||
<nav id="sidebar" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
|
||||
<div class="position-sticky pt-3">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= ($current_page == 'index.php' || $current_page == '') ? 'active' : '' ?>" aria-current="page" href="/">
|
||||
<i class="bi bi-kanban"></i>
|
||||
<span class="nav-link-text">Pulpit procesów</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= ($current_page == 'calendar.php') ? 'active' : '' ?>" href="calendar.php">
|
||||
<i class="bi bi-calendar-event"></i>
|
||||
<span class="nav-link-text">Kalendarz</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="#settings-submenu" data-bs-toggle="collapse" aria-expanded="false">
|
||||
<i class="bi bi-gear"></i>
|
||||
<span class="nav-link-text">Ustawienia</span>
|
||||
</a>
|
||||
<ul class="nav flex-column collapse" id="settings-submenu" data-bs-parent="#sidebar">
|
||||
<li class="nav-item submenu-item">
|
||||
<a class="nav-link <?= ($current_page == 'event_types.php') ? 'active' : '' ?>" href="event_types.php">
|
||||
<i class="bi bi-tags"></i>
|
||||
<span class="nav-link-text">Typy zdarzeń</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item submenu-item">
|
||||
<a class="nav-link <?= ($current_page == 'bni_groups.php') ? 'active' : '' ?>" href="bni_groups.php">
|
||||
<i class="bi bi-people"></i>
|
||||
<span class="nav-link-text">Grupy BNI</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item submenu-item">
|
||||
<a class="nav-link <?= ($current_page == 'functions.php') ? 'active' : '' ?>" href="functions.php">
|
||||
<i class="bi bi-person-rolodex"></i>
|
||||
<span class="nav-link-text">Funkcje</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= ($current_page == 'process_definitions.php') ? 'active' : '' ?>" href="process_definitions.php">
|
||||
<i class="bi bi-diagram-3"></i>
|
||||
<span class="nav-link-text">Definicje procesów</span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
34
_update_bni_group.php
Normal file
34
_update_bni_group.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
if (isset($_POST['edit'])) {
|
||||
$id = $_POST['id'];
|
||||
$name = $_POST['name'];
|
||||
$city = $_POST['city'] ?? null;
|
||||
$active = isset($_POST['active']) ? 1 : 0;
|
||||
$display_order = $_POST['display_order'] ?? 0;
|
||||
|
||||
if (empty($name) || empty($id)) {
|
||||
$_SESSION['error_message'] = 'Name and ID are required.';
|
||||
header('Location: bni_groups.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("UPDATE bni_groups SET name = :name, city = :city, active = :active, display_order = :display_order WHERE id = :id");
|
||||
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':name', $name);
|
||||
$stmt->bindParam(':city', $city);
|
||||
$stmt->bindParam(':active', $active, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':display_order', $display_order, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
$_SESSION['success_message'] = 'BNI Group updated successfully!';
|
||||
} catch (PDOException $e) {
|
||||
$_SESSION['error_message'] = 'Error updating BNI group: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: bni_groups.php');
|
||||
exit;
|
||||
67
_update_bni_group_order.php
Normal file
67
_update_bni_group_order.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
|
||||
session_start();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['order']) && is_array($_POST['order'])) {
|
||||
$ordered_ids = $_POST['order'];
|
||||
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($ordered_ids as $index => $id) {
|
||||
$sql = "UPDATE bni_groups SET display_order = ? WHERE id = ?";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$index + 1, $id]);
|
||||
}
|
||||
$pdo->commit();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => true, 'message' => 'Order updated successfully.']);
|
||||
exit();
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$pdo->rollBack();
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Error updating display order: ' . $e->getMessage()]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for old form submission, though it's being deprecated
|
||||
if (isset($_POST['ids']) && isset($_POST['display_order'])) {
|
||||
$ids = $_POST['ids'];
|
||||
$display_orders = $_POST['display_order'];
|
||||
|
||||
if (count($ids) !== count($display_orders)) {
|
||||
$_SESSION['error_message'] = "Something went wrong. Please try again.";
|
||||
header("Location: bni_groups.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
for ($i = 0; $i < count($ids); $i++) {
|
||||
$sql = "UPDATE bni_groups SET display_order = ? WHERE id = ?";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$display_orders[$i], $ids[$i]]);
|
||||
}
|
||||
$pdo->commit();
|
||||
$_SESSION['success_message'] = "Display order updated successfully.";
|
||||
} catch (PDOException $e) {
|
||||
$pdo->rollBack();
|
||||
$_SESSION['error_message'] = "Error updating display order: " . $e->getMessage();
|
||||
}
|
||||
|
||||
header("Location: bni_groups.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid request.']);
|
||||
81
_update_calendar_event.php
Normal file
81
_update_calendar_event.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once 'db/config.php';
|
||||
require_once 'lib/ErrorHandler.php';
|
||||
require_once 'lib/WorkflowExceptions.php';
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
throw new WorkflowNotAllowedException('Authentication required.');
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$event_id = $_POST['event_id'] ?? null;
|
||||
$title = $_POST['title'] ?? '';
|
||||
$description = $_POST['description'] ?? '';
|
||||
$start_datetime = $_POST['start_datetime'] ?? '';
|
||||
$end_datetime = $_POST['end_datetime'] ?? '';
|
||||
$event_type_id = $_POST['event_type_id'] ?? null;
|
||||
$update_scope = $_POST['update_scope'] ?? 'one';
|
||||
$group_ids = $_POST['group_ids'] ?? [];
|
||||
|
||||
if (empty($event_id) || empty($title) || empty($event_type_id) || !is_array($group_ids)) {
|
||||
throw new WorkflowRuleFailedException('Empty fields');
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$event_ids_to_update = [];
|
||||
|
||||
if ($update_scope === 'all') {
|
||||
// Find the parent event id
|
||||
$stmt = $pdo->prepare("SELECT parent_event_id, recurrence FROM calendar_events WHERE id = ?");
|
||||
$stmt->execute([$event_id]);
|
||||
$event = $stmt->fetch();
|
||||
|
||||
$parent_event_id = $event['parent_event_id'] ?? $event_id;
|
||||
|
||||
// Get all event ids in the series
|
||||
$stmt = $pdo->prepare("SELECT id FROM calendar_events WHERE id = ? OR parent_event_id = ?");
|
||||
$stmt->execute([$parent_event_id, $parent_event_id]);
|
||||
$event_ids_to_update = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
} else {
|
||||
$event_ids_to_update[] = $event_id;
|
||||
}
|
||||
|
||||
// Prepare statements
|
||||
$stmt_update_event = $pdo->prepare("UPDATE calendar_events SET title = ?, description = ?, event_type_id = ? WHERE id = ?");
|
||||
if($update_scope === 'one'){
|
||||
$stmt_update_event = $pdo->prepare("UPDATE calendar_events SET title = ?, description = ?, start_datetime = ?, end_datetime = ?, event_type_id = ? WHERE id = ?");
|
||||
}
|
||||
|
||||
$stmt_delete_groups = $pdo->prepare("DELETE FROM calendar_event_groups WHERE calendar_event_id = ?");
|
||||
$stmt_add_groups = $pdo->prepare("INSERT INTO calendar_event_groups (calendar_event_id, bni_group_id) VALUES (?, ?)");
|
||||
|
||||
foreach ($event_ids_to_update as $id) {
|
||||
// Update event details
|
||||
if($update_scope === 'one'){
|
||||
$stmt_update_event->execute([$title, $description, $start_datetime, $end_datetime, $event_type_id, $id]);
|
||||
} else {
|
||||
$stmt_update_event->execute([$title, $description, $event_type_id, $id]);
|
||||
}
|
||||
|
||||
// Update group associations
|
||||
$stmt_delete_groups->execute([$id]);
|
||||
foreach ($group_ids as $group_id) {
|
||||
$stmt_add_groups->execute([$id, $group_id]);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
header("Location: calendar.php");
|
||||
exit();
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
error_log("Error updating event: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
19
_update_event_type.php
Normal file
19
_update_event_type.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
|
||||
if (isset($_POST['edit'])) {
|
||||
$id = $_POST['id'];
|
||||
$name = $_POST['name'];
|
||||
$color = $_POST['color'];
|
||||
$display_order = $_POST['display_order'];
|
||||
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("UPDATE event_types SET name = ?, color = ?, display_order = ? WHERE id = ?");
|
||||
$stmt->execute([$name, $color, $display_order, $id]);
|
||||
|
||||
session_start();
|
||||
$_SESSION['success_message'] = 'Event type updated successfully.';
|
||||
header('Location: event_types.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
67
_update_event_type_order.php
Normal file
67
_update_event_type_order.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
|
||||
session_start();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['order']) && is_array($_POST['order'])) {
|
||||
$ordered_ids = $_POST['order'];
|
||||
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($ordered_ids as $index => $id) {
|
||||
$sql = "UPDATE event_types SET display_order = ? WHERE id = ?";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$index + 1, $id]);
|
||||
}
|
||||
$pdo->commit();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => true, 'message' => 'Order updated successfully.']);
|
||||
exit();
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$pdo->rollBack();
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Error updating display order: ' . $e->getMessage()]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for old form submission, though it's being deprecated
|
||||
if (isset($_POST['ids']) && isset($_POST['display_order'])) {
|
||||
$ids = $_POST['ids'];
|
||||
$display_orders = $_POST['display_order'];
|
||||
|
||||
if (count($ids) !== count($display_orders)) {
|
||||
$_SESSION['error_message'] = "Something went wrong. Please try again.";
|
||||
header("Location: event_types.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
for ($i = 0; $i < count($ids); $i++) {
|
||||
$sql = "UPDATE event_types SET display_order = ? WHERE id = ?";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$display_orders[$i], $ids[$i]]);
|
||||
}
|
||||
$pdo->commit();
|
||||
$_SESSION['success_message'] = "Display order updated successfully.";
|
||||
} catch (PDOException $e) {
|
||||
$pdo->rollBack();
|
||||
$_SESSION['error_message'] = "Error updating display order: " . $e->getMessage();
|
||||
}
|
||||
|
||||
header("Location: event_types.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid request.']);
|
||||
30
_update_function.php
Normal file
30
_update_function.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['user_id']) || $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: login.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once 'db/config.php';
|
||||
|
||||
if (isset($_POST['id'], $_POST['name'], $_POST['bni_group_id'])) {
|
||||
$id = $_POST['id'];
|
||||
$name = trim($_POST['name']);
|
||||
$bni_group_id = trim($_POST['bni_group_id']);
|
||||
|
||||
if (!empty($name) && !empty($bni_group_id)) {
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("UPDATE functions SET name = :name, bni_group_id = :bni_group_id WHERE id = :id");
|
||||
$stmt->execute(['name' => $name, 'bni_group_id' => $bni_group_id, 'id' => $id]);
|
||||
$_SESSION['success_message'] = "Function updated successfully.";
|
||||
} catch (PDOException $e) {
|
||||
$_SESSION['error_message'] = "Error updating function: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$_SESSION['error_message'] = "Name and group are required.";
|
||||
}
|
||||
|
||||
header('Location: functions.php');
|
||||
exit();
|
||||
67
_update_function_order.php
Normal file
67
_update_function_order.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
|
||||
session_start();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['order']) && is_array($_POST['order'])) {
|
||||
$ordered_ids = $_POST['order'];
|
||||
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($ordered_ids as $index => $id) {
|
||||
$sql = "UPDATE functions SET display_order = ? WHERE id = ?";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$index + 1, $id]);
|
||||
}
|
||||
$pdo->commit();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => true, 'message' => 'Order updated successfully.']);
|
||||
exit();
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$pdo->rollBack();
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Error updating display order: ' . $e->getMessage()]);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for old form submission, though it's being deprecated
|
||||
if (isset($_POST['ids']) && isset($_POST['display_order'])) {
|
||||
$ids = $_POST['ids'];
|
||||
$display_orders = $_POST['display_order'];
|
||||
|
||||
if (count($ids) !== count($display_orders)) {
|
||||
$_SESSION['error_message'] = "Something went wrong. Please try again.";
|
||||
header("Location: functions.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
for ($i = 0; $i < count($ids); $i++) {
|
||||
$sql = "UPDATE functions SET display_order = ? WHERE id = ?";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$display_orders[$i], $ids[$i]]);
|
||||
}
|
||||
$pdo->commit();
|
||||
$_SESSION['success_message'] = "Display order updated successfully.";
|
||||
} catch (PDOException $e) {
|
||||
$pdo->rollBack();
|
||||
$_SESSION['error_message'] = "Error updating display order: " . $e->getMessage();
|
||||
}
|
||||
|
||||
header("Location: functions.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid request.']);
|
||||
29
_update_instance_status.php
Normal file
29
_update_instance_status.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
require_once 'lib/ErrorHandler.php';
|
||||
require_once 'WorkflowEngine.php';
|
||||
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
throw new WorkflowNotAllowedException('Unauthorized');
|
||||
}
|
||||
|
||||
$instanceId = $_POST['instance_id'] ?? null;
|
||||
$status = $_POST['status'] ?? null;
|
||||
$reason = $_POST['reason'] ?? '';
|
||||
$userId = $_SESSION['user_id'];
|
||||
|
||||
if (!$instanceId || !$status) {
|
||||
throw new WorkflowRuleFailedException('Missing parameters: instance_id and status are required.');
|
||||
}
|
||||
|
||||
$workflowEngine = new WorkflowEngine();
|
||||
$workflowEngine->applyManualStatus((int)$instanceId, $status, $reason, (int)$userId);
|
||||
|
||||
if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['message' => 'Status updated successfully.']);
|
||||
} else {
|
||||
header('Location: index.php');
|
||||
}
|
||||
exit;
|
||||
47
_update_meeting_attendance.php
Normal file
47
_update_meeting_attendance.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
require_once 'lib/ErrorHandler.php';
|
||||
register_error_handler();
|
||||
|
||||
require_once 'db/config.php';
|
||||
require_once 'WorkflowEngine.php';
|
||||
|
||||
session_start();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
throw new WorkflowException('You must be logged in to perform this action.', 401);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
throw new WorkflowException('Invalid request method.', 405);
|
||||
}
|
||||
|
||||
$personId = $_POST['person_id'] ?? null;
|
||||
$bniGroupId = $_POST['bni_group_id'] ?? $_POST['group_id'] ?? null;
|
||||
$meetingDate = $_POST['meeting_date'] ?? null;
|
||||
$status = $_POST['attendance_status'] ?? null;
|
||||
$guestSurvey = $_POST['guest_survey'] ?? null;
|
||||
$userId = $_SESSION['user_id'];
|
||||
|
||||
if (!$personId || !$bniGroupId || !$meetingDate || !$status) {
|
||||
$missing_params = [];
|
||||
if (!$personId) $missing_params[] = 'person_id';
|
||||
if (!$bniGroupId) $missing_params[] = 'bni_group_id';
|
||||
if (!$meetingDate) $missing_params[] = 'meeting_date';
|
||||
if (!$status) $missing_params[] = 'status';
|
||||
throw new WorkflowException('Missing required parameters: ' . implode(', ', $missing_params), 400);
|
||||
}
|
||||
|
||||
$workflowEngine = new WorkflowEngine();
|
||||
|
||||
$meetingId = $workflowEngine->getOrCreateMeeting((int)$bniGroupId, $meetingDate);
|
||||
$workflowEngine->updateMeetingAttendance($meetingId, (int)$personId, (int)$bniGroupId, $status, (int)$userId, $guestSurvey);
|
||||
|
||||
$response = [
|
||||
'success' => true,
|
||||
'message' => 'Attendance updated successfully.'
|
||||
];
|
||||
|
||||
echo json_encode($response);
|
||||
|
||||
124
_update_person.php
Normal file
124
_update_person.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
session_start();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$personId = $_POST['id'];
|
||||
$first_name = $_POST['first_name'];
|
||||
$last_name = $_POST['last_name'];
|
||||
$email = $_POST['email'];
|
||||
$company_name = $_POST['company_name'];
|
||||
$phone = $_POST['phone'];
|
||||
$role = $_POST['role'] ?? 'guest';
|
||||
$functions = isset($_POST['functions']) ? $_POST['functions'] : [];
|
||||
$password = $_POST['password'];
|
||||
$bni_group_id = isset($_POST['bni_group_id']) && !empty($_POST['bni_group_id']) ? $_POST['bni_group_id'] : null;
|
||||
|
||||
// New fields
|
||||
$nip = $_POST['nip'] ?? null;
|
||||
$industry = $_POST['industry'] ?? null;
|
||||
$company_size_revenue = $_POST['company_size_revenue'] ?? null;
|
||||
$business_description = $_POST['business_description'] ?? null;
|
||||
|
||||
if (empty($first_name) || empty($last_name) || empty($email)) {
|
||||
http_response_code(422);
|
||||
echo json_encode(['error' => ['message' => 'First name, last name, and email are required.'], 'correlation_id' => uniqid()]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Only members can be in a group
|
||||
if ($role !== 'member') {
|
||||
$bni_group_id = null;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Handle file uploads
|
||||
$upload_dir = 'uploads/people/' . $personId . '/';
|
||||
if (!is_dir($upload_dir)) {
|
||||
mkdir($upload_dir, 0777, true);
|
||||
}
|
||||
|
||||
$file_fields = [
|
||||
'company_logo' => 'company_logo_path',
|
||||
'person_photo' => 'person_photo_path',
|
||||
'gains_sheet' => 'gains_sheet_path',
|
||||
'top_wanted_contacts' => 'top_wanted_contacts_path',
|
||||
'top_owned_contacts' => 'top_owned_contacts_path'
|
||||
];
|
||||
|
||||
$file_paths = [];
|
||||
|
||||
foreach ($file_fields as $form_field_name => $db_column_name) {
|
||||
if (isset($_FILES[$form_field_name]) && $_FILES[$form_field_name]['error'] == UPLOAD_ERR_OK) {
|
||||
$tmp_name = $_FILES[$form_field_name]['tmp_name'];
|
||||
$original_name = basename($_FILES[$form_field_name]['name']);
|
||||
$file_ext = pathinfo($original_name, PATHINFO_EXTENSION);
|
||||
$new_filename = uniqid($form_field_name . '_', true) . '.' . $file_ext;
|
||||
$destination = $upload_dir . $new_filename;
|
||||
|
||||
if (move_uploaded_file($tmp_name, $destination)) {
|
||||
$file_paths[$db_column_name] = $destination;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare SQL for updating person details
|
||||
$sql_parts = [
|
||||
'first_name = ?', 'last_name = ?', 'email = ?', 'company_name = ?', 'phone = ?',
|
||||
'role = ?', 'bni_group_id = ?', 'nip = ?', 'industry = ?', 'company_size_revenue = ?',
|
||||
'business_description = ?'
|
||||
];
|
||||
$params = [
|
||||
$first_name, $last_name, $email, $company_name, $phone, $role, $bni_group_id,
|
||||
$nip, $industry, $company_size_revenue, $business_description
|
||||
];
|
||||
|
||||
if (!empty($password)) {
|
||||
$sql_parts[] = 'password = ?';
|
||||
$params[] = password_hash($password, PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
foreach ($file_paths as $column => $path) {
|
||||
$sql_parts[] = "$column = ?";
|
||||
$params[] = $path;
|
||||
}
|
||||
|
||||
$sql = "UPDATE people SET " . implode(', ', $sql_parts) . " WHERE id = ?";
|
||||
$params[] = $personId;
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
// Update functions
|
||||
$stmt = $pdo->prepare("DELETE FROM user_functions WHERE user_id = ?");
|
||||
$stmt->execute([$personId]);
|
||||
|
||||
if (!empty($functions)) {
|
||||
$sql = "INSERT INTO user_functions (user_id, function_id) VALUES (?, ?)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
foreach ($functions as $functionId) {
|
||||
$stmt->execute([$personId, $functionId]);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
$_SESSION['success_message'] = 'Osoba zaktualizowana pomyślnie.';
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$pdo->rollBack();
|
||||
error_log('Update failed: ' . $e->getMessage());
|
||||
$_SESSION['error_message'] = "Błąd podczas aktualizacji osoby: " . $e->getMessage();
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log('File upload or other error: ' . $e->getMessage());
|
||||
$_SESSION['error_message'] = "Błąd: " . $e->getMessage();
|
||||
}
|
||||
|
||||
header('Location: index.php');
|
||||
exit();
|
||||
}
|
||||
36
_update_training_checklist_status.php
Normal file
36
_update_training_checklist_status.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
require_once 'lib/ErrorHandler.php';
|
||||
require_once 'WorkflowEngine.php';
|
||||
|
||||
session_start();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
throw new WorkflowNotAllowedException('Unauthorized');
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
throw new WorkflowNotAllowedException('Method Not Allowed');
|
||||
}
|
||||
|
||||
$inputJSON = file_get_contents('php://input');
|
||||
$input = json_decode($inputJSON, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new WorkflowRuleFailedException('Invalid JSON');
|
||||
}
|
||||
|
||||
$instanceId = $input['instance_id'] ?? null;
|
||||
$taskCode = $input['task_code'] ?? null;
|
||||
$isChecked = $input['is_checked'] ?? null;
|
||||
$userId = $_SESSION['user_id'];
|
||||
|
||||
if (!$instanceId || !$taskCode || $isChecked === null) {
|
||||
throw new WorkflowRuleFailedException('Missing required parameters: instance_id, task_code, is_checked');
|
||||
}
|
||||
|
||||
$workflowEngine = new WorkflowEngine();
|
||||
$result = $workflowEngine->updateChecklistStatus((int)$instanceId, $taskCode, (bool)$isChecked, (int)$userId);
|
||||
echo json_encode($result);
|
||||
exit;
|
||||
493
ai/LocalAIApi.php
Normal file
493
ai/LocalAIApi.php
Normal file
@ -0,0 +1,493 @@
|
||||
<?php
|
||||
// LocalAIApi — proxy client for the Responses API.
|
||||
// Usage (async: auto-polls status until ready):
|
||||
// require_once __DIR__ . '/ai/LocalAIApi.php';
|
||||
// $response = LocalAIApi::createResponse([
|
||||
// 'input' => [
|
||||
// ['role' => 'system', 'content' => 'You are a helpful assistant.'],
|
||||
// ['role' => 'user', 'content' => 'Tell me a bedtime story.'],
|
||||
// ],
|
||||
// ]);
|
||||
// if (!empty($response['success'])) {
|
||||
// // response['data'] contains full payload, e.g.:
|
||||
// // {
|
||||
// // "id": "resp_xxx",
|
||||
// // "status": "completed",
|
||||
// // "output": [
|
||||
// // {"type": "reasoning", "summary": []},
|
||||
// // {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]}
|
||||
// // ]
|
||||
// // }
|
||||
// $decoded = LocalAIApi::decodeJsonFromResponse($response); // or inspect $response['data'] / extractText(...)
|
||||
// }
|
||||
// Poll settings override:
|
||||
// LocalAIApi::createResponse($payload, ['poll_interval' => 5, 'poll_timeout' => 300]);
|
||||
|
||||
class LocalAIApi
|
||||
{
|
||||
/** @var array<string,mixed>|null */
|
||||
private static ?array $configCache = null;
|
||||
|
||||
/**
|
||||
* Signature compatible with the OpenAI Responses API.
|
||||
*
|
||||
* @param array<string,mixed> $params Request body (model, input, text, reasoning, metadata, etc.).
|
||||
* @param array<string,mixed> $options Extra options (timeout, verify_tls, headers, path, project_uuid).
|
||||
* @return array{
|
||||
* success:bool,
|
||||
* status?:int,
|
||||
* data?:mixed,
|
||||
* error?:string,
|
||||
* response?:mixed,
|
||||
* message?:string
|
||||
* }
|
||||
*/
|
||||
public static function createResponse(array $params, array $options = []): array
|
||||
{
|
||||
$cfg = self::config();
|
||||
$payload = $params;
|
||||
|
||||
if (empty($payload['input']) || !is_array($payload['input'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'input_missing',
|
||||
'message' => 'Parameter "input" is required and must be an array.',
|
||||
];
|
||||
}
|
||||
|
||||
if (!isset($payload['model']) || $payload['model'] === '') {
|
||||
$payload['model'] = $cfg['default_model'];
|
||||
}
|
||||
|
||||
$initial = self::request($options['path'] ?? null, $payload, $options);
|
||||
if (empty($initial['success'])) {
|
||||
return $initial;
|
||||
}
|
||||
|
||||
// Async flow: if backend returns ai_request_id, poll status until ready
|
||||
$data = $initial['data'] ?? null;
|
||||
if (is_array($data) && isset($data['ai_request_id'])) {
|
||||
$aiRequestId = $data['ai_request_id'];
|
||||
$pollTimeout = isset($options['poll_timeout']) ? (int) $options['poll_timeout'] : 300; // seconds
|
||||
$pollInterval = isset($options['poll_interval']) ? (int) $options['poll_interval'] : 5; // seconds
|
||||
return self::awaitResponse($aiRequestId, [
|
||||
'timeout' => $pollTimeout,
|
||||
'interval' => $pollInterval,
|
||||
'headers' => $options['headers'] ?? [],
|
||||
'timeout_per_call' => $options['timeout'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
return $initial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snake_case alias for createResponse (matches the provided example).
|
||||
*
|
||||
* @param array<string,mixed> $params
|
||||
* @param array<string,mixed> $options
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function create_response(array $params, array $options = []): array
|
||||
{
|
||||
return self::createResponse($params, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a raw request to the AI proxy.
|
||||
*
|
||||
* @param string $path Endpoint (may be an absolute URL).
|
||||
* @param array<string,mixed> $payload JSON payload.
|
||||
* @param array<string,mixed> $options Additional request options.
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function request(?string $path = null, array $payload = [], array $options = []): array
|
||||
{
|
||||
$cfg = self::config();
|
||||
|
||||
$projectUuid = $cfg['project_uuid'];
|
||||
if (empty($projectUuid)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'project_uuid_missing',
|
||||
'message' => 'PROJECT_UUID is not defined; aborting AI request.',
|
||||
];
|
||||
}
|
||||
|
||||
$defaultPath = $cfg['responses_path'] ?? null;
|
||||
$resolvedPath = $path ?? ($options['path'] ?? $defaultPath);
|
||||
if (empty($resolvedPath)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'project_id_missing',
|
||||
'message' => 'PROJECT_ID is not defined; cannot resolve AI proxy endpoint.',
|
||||
];
|
||||
}
|
||||
|
||||
$url = self::buildUrl($resolvedPath, $cfg['base_url']);
|
||||
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30;
|
||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
|
||||
if ($timeout <= 0) {
|
||||
$timeout = 30;
|
||||
}
|
||||
|
||||
$baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true;
|
||||
$verifyTls = array_key_exists('verify_tls', $options)
|
||||
? (bool) $options['verify_tls']
|
||||
: $baseVerifyTls;
|
||||
|
||||
$projectHeader = $cfg['project_header'];
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json',
|
||||
];
|
||||
$headers[] = $projectHeader . ': ' . $projectUuid;
|
||||
if (!empty($options['headers']) && is_array($options['headers'])) {
|
||||
foreach ($options['headers'] as $header) {
|
||||
if (is_string($header) && $header !== '') {
|
||||
$headers[] = $header;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($projectUuid) && !array_key_exists('project_uuid', $payload)) {
|
||||
$payload['project_uuid'] = $projectUuid;
|
||||
}
|
||||
|
||||
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
if ($body === false) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'json_encode_failed',
|
||||
'message' => 'Failed to encode request body to JSON.',
|
||||
];
|
||||
}
|
||||
|
||||
return self::sendCurl($url, 'POST', $body, $headers, $timeout, $verifyTls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll AI request status until ready or timeout.
|
||||
*
|
||||
* @param int|string $aiRequestId
|
||||
* @param array<string,mixed> $options
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function awaitResponse($aiRequestId, array $options = []): array
|
||||
{
|
||||
$cfg = self::config();
|
||||
|
||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : 300; // seconds
|
||||
$interval = isset($options['interval']) ? (int) $options['interval'] : 5; // seconds
|
||||
if ($interval <= 0) {
|
||||
$interval = 5;
|
||||
}
|
||||
$perCallTimeout = isset($options['timeout_per_call']) ? (int) $options['timeout_per_call'] : null;
|
||||
|
||||
$deadline = time() + max($timeout, $interval);
|
||||
$headers = $options['headers'] ?? [];
|
||||
|
||||
while (true) {
|
||||
$statusResp = self::fetchStatus($aiRequestId, [
|
||||
'headers' => $headers,
|
||||
'timeout' => $perCallTimeout,
|
||||
]);
|
||||
if (!empty($statusResp['success'])) {
|
||||
$data = $statusResp['data'] ?? [];
|
||||
if (is_array($data)) {
|
||||
$statusValue = $data['status'] ?? null;
|
||||
if ($statusValue === 'success') {
|
||||
return [
|
||||
'success' => true,
|
||||
'status' => 200,
|
||||
'data' => $data['response'] ?? $data,
|
||||
];
|
||||
}
|
||||
if ($statusValue === 'failed') {
|
||||
return [
|
||||
'success' => false,
|
||||
'status' => 500,
|
||||
'error' => isset($data['error']) ? (string)$data['error'] : 'AI request failed',
|
||||
'data' => $data,
|
||||
];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return $statusResp;
|
||||
}
|
||||
|
||||
if (time() >= $deadline) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'timeout',
|
||||
'message' => 'Timed out waiting for AI response.',
|
||||
];
|
||||
}
|
||||
sleep($interval);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch status for queued AI request.
|
||||
*
|
||||
* @param int|string $aiRequestId
|
||||
* @param array<string,mixed> $options
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public static function fetchStatus($aiRequestId, array $options = []): array
|
||||
{
|
||||
$cfg = self::config();
|
||||
$projectUuid = $cfg['project_uuid'];
|
||||
if (empty($projectUuid)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'project_uuid_missing',
|
||||
'message' => 'PROJECT_UUID is not defined; aborting status check.',
|
||||
];
|
||||
}
|
||||
|
||||
$statusPath = self::resolveStatusPath($aiRequestId, $cfg);
|
||||
$url = self::buildUrl($statusPath, $cfg['base_url']);
|
||||
|
||||
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30;
|
||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
|
||||
if ($timeout <= 0) {
|
||||
$timeout = 30;
|
||||
}
|
||||
|
||||
$baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true;
|
||||
$verifyTls = array_key_exists('verify_tls', $options)
|
||||
? (bool) $options['verify_tls']
|
||||
: $baseVerifyTls;
|
||||
|
||||
$projectHeader = $cfg['project_header'];
|
||||
$headers = [
|
||||
'Accept: application/json',
|
||||
$projectHeader . ': ' . $projectUuid,
|
||||
];
|
||||
if (!empty($options['headers']) && is_array($options['headers'])) {
|
||||
foreach ($options['headers'] as $header) {
|
||||
if (is_string($header) && $header !== '') {
|
||||
$headers[] = $header;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::sendCurl($url, 'GET', null, $headers, $timeout, $verifyTls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract plain text from a Responses API payload.
|
||||
*
|
||||
* @param array<string,mixed> $response Result of LocalAIApi::createResponse|request.
|
||||
* @return string
|
||||
*/
|
||||
public static function extractText(array $response): string
|
||||
{
|
||||
$payload = $response['data'] ?? $response;
|
||||
if (!is_array($payload)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!empty($payload['output']) && is_array($payload['output'])) {
|
||||
$combined = '';
|
||||
foreach ($payload['output'] as $item) {
|
||||
if (!isset($item['content']) || !is_array($item['content'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($item['content'] as $block) {
|
||||
if (is_array($block) && ($block['type'] ?? '') === 'output_text' && !empty($block['text'])) {
|
||||
$combined .= $block['text'];
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($combined !== '') {
|
||||
return $combined;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($payload['choices'][0]['message']['content'])) {
|
||||
return (string) $payload['choices'][0]['message']['content'];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to decode JSON emitted by the model (handles markdown fences).
|
||||
*
|
||||
* @param array<string,mixed> $response
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
public static function decodeJsonFromResponse(array $response): ?array
|
||||
{
|
||||
$text = self::extractText($response);
|
||||
if ($text === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$decoded = json_decode($text, true);
|
||||
if (is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
$stripped = preg_replace('/^```json|```$/m', '', trim($text));
|
||||
if ($stripped !== null && $stripped !== $text) {
|
||||
$decoded = json_decode($stripped, true);
|
||||
if (is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from ai/config.php.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function config(): array
|
||||
{
|
||||
if (self::$configCache === null) {
|
||||
$configPath = __DIR__ . '/config.php';
|
||||
if (!file_exists($configPath)) {
|
||||
throw new RuntimeException('AI config file not found: ai/config.php');
|
||||
}
|
||||
$cfg = require $configPath;
|
||||
if (!is_array($cfg)) {
|
||||
throw new RuntimeException('Invalid AI config format: expected array');
|
||||
}
|
||||
self::$configCache = $cfg;
|
||||
}
|
||||
|
||||
return self::$configCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an absolute URL from base_url and a path.
|
||||
*/
|
||||
private static function buildUrl(string $path, string $baseUrl): string
|
||||
{
|
||||
$trimmed = trim($path);
|
||||
if ($trimmed === '') {
|
||||
return $baseUrl;
|
||||
}
|
||||
if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) {
|
||||
return $trimmed;
|
||||
}
|
||||
if ($trimmed[0] === '/') {
|
||||
return $baseUrl . $trimmed;
|
||||
}
|
||||
return $baseUrl . '/' . $trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve status path based on configured responses_path and ai_request_id.
|
||||
*
|
||||
* @param int|string $aiRequestId
|
||||
* @param array<string,mixed> $cfg
|
||||
* @return string
|
||||
*/
|
||||
private static function resolveStatusPath($aiRequestId, array $cfg): string
|
||||
{
|
||||
$basePath = $cfg['responses_path'] ?? '';
|
||||
$trimmed = rtrim($basePath, '/');
|
||||
if ($trimmed === '') {
|
||||
return '/ai-request/' . rawurlencode((string)$aiRequestId) . '/status';
|
||||
}
|
||||
if (substr($trimmed, -11) !== '/ai-request') {
|
||||
$trimmed .= '/ai-request';
|
||||
}
|
||||
return $trimmed . '/' . rawurlencode((string)$aiRequestId) . '/status';
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared CURL sender for GET/POST requests.
|
||||
*
|
||||
* @param string $url
|
||||
* @param string $method
|
||||
* @param string|null $body
|
||||
* @param array<int,string> $headers
|
||||
* @param int $timeout
|
||||
* @param bool $verifyTls
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function sendCurl(string $url, string $method, ?string $body, array $headers, int $timeout, bool $verifyTls): array
|
||||
{
|
||||
if (!function_exists('curl_init')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'curl_missing',
|
||||
'message' => 'PHP cURL extension is missing. Install or enable it on the VM.',
|
||||
];
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifyTls);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0);
|
||||
curl_setopt($ch, CURLOPT_FAILONERROR, false);
|
||||
|
||||
$upper = strtoupper($method);
|
||||
if ($upper === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body ?? '');
|
||||
} else {
|
||||
curl_setopt($ch, CURLOPT_HTTPGET, true);
|
||||
}
|
||||
|
||||
$responseBody = curl_exec($ch);
|
||||
if ($responseBody === false) {
|
||||
$error = curl_error($ch) ?: 'Unknown cURL error';
|
||||
curl_close($ch);
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'curl_error',
|
||||
'message' => $error,
|
||||
];
|
||||
}
|
||||
|
||||
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$decoded = null;
|
||||
if ($responseBody !== '' && $responseBody !== null) {
|
||||
$decoded = json_decode($responseBody, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$decoded = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($status >= 200 && $status < 300) {
|
||||
return [
|
||||
'success' => true,
|
||||
'status' => $status,
|
||||
'data' => $decoded ?? $responseBody,
|
||||
];
|
||||
}
|
||||
|
||||
$errorMessage = 'AI proxy request failed';
|
||||
if (is_array($decoded)) {
|
||||
$errorMessage = $decoded['error'] ?? $decoded['message'] ?? $errorMessage;
|
||||
} elseif (is_string($responseBody) && $responseBody !== '') {
|
||||
$errorMessage = $responseBody;
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'status' => $status,
|
||||
'error' => $errorMessage,
|
||||
'response' => $decoded ?? $responseBody,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy alias for backward compatibility with the previous class name.
|
||||
if (!class_exists('OpenAIService')) {
|
||||
class_alias(LocalAIApi::class, 'OpenAIService');
|
||||
}
|
||||
52
ai/config.php
Normal file
52
ai/config.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
// OpenAI proxy configuration (workspace scope).
|
||||
// Reads values from environment variables or executor/.env.
|
||||
|
||||
$projectUuid = getenv('PROJECT_UUID');
|
||||
$projectId = getenv('PROJECT_ID');
|
||||
|
||||
if (
|
||||
($projectUuid === false || $projectUuid === null || $projectUuid === '') ||
|
||||
($projectId === false || $projectId === null || $projectId === '')
|
||||
) {
|
||||
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
||||
if ($envPath && is_readable($envPath)) {
|
||||
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
if (!str_contains($line, '=')) {
|
||||
continue;
|
||||
}
|
||||
[$key, $value] = array_map('trim', explode('=', $line, 2));
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
$value = trim($value, "\"' ");
|
||||
if (getenv($key) === false || getenv($key) === '') {
|
||||
putenv("{$key}={$value}");
|
||||
}
|
||||
}
|
||||
$projectUuid = getenv('PROJECT_UUID');
|
||||
$projectId = getenv('PROJECT_ID');
|
||||
}
|
||||
}
|
||||
|
||||
$projectUuid = ($projectUuid === false) ? null : $projectUuid;
|
||||
$projectId = ($projectId === false) ? null : $projectId;
|
||||
|
||||
$baseUrl = 'https://flatlogic.com';
|
||||
$responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null;
|
||||
|
||||
return [
|
||||
'base_url' => $baseUrl,
|
||||
'responses_path' => $responsesPath,
|
||||
'project_id' => $projectId,
|
||||
'project_uuid' => $projectUuid,
|
||||
'project_header' => 'project-uuid',
|
||||
'default_model' => 'gpt-5-mini',
|
||||
'timeout' => 30,
|
||||
'verify_tls' => true,
|
||||
];
|
||||
169
assets/css/custom.css
Normal file
169
assets/css/custom.css
Normal file
@ -0,0 +1,169 @@
|
||||
body {
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
width: 250px;
|
||||
transition: width 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
#main-content {
|
||||
margin-left: 250px;
|
||||
transition: margin-left 0.3s ease-in-out;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
body.sidebar-collapsed #sidebar {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
body.sidebar-collapsed #main-content {
|
||||
margin-left: 80px;
|
||||
}
|
||||
|
||||
body.sidebar-collapsed #sidebar .nav-link-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.sidebar-collapsed #sidebar .nav-link i {
|
||||
font-size: 1.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
body.sidebar-collapsed #sidebar .nav-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-link i {
|
||||
margin-right: 10px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
color: #0d6efd;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
padding-top: .75rem;
|
||||
padding-bottom: .75rem;
|
||||
font-size: 1rem;
|
||||
/* background-color: rgba(0, 0, 0, .25); */
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.navbar .form-control {
|
||||
padding: .75rem 1rem;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Calendar styles */
|
||||
.calendar {
|
||||
table-layout: fixed;
|
||||
}
|
||||
.calendar td {
|
||||
height: 120px;
|
||||
vertical-align: top;
|
||||
border: 1px solid #ddd;
|
||||
padding: 4px;
|
||||
}
|
||||
.calendar .day-number {
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.calendar .not-month {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.events {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.event {
|
||||
font-size: 0.75rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.submenu-item .nav-link {
|
||||
padding-left: 2.5rem;
|
||||
}
|
||||
|
||||
.modal-fullscreen-xl {
|
||||
width: 95%;
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.person-cell {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.person-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.person-details {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.person-details .person-group {
|
||||
font-weight: bold;
|
||||
color: #198754;
|
||||
}
|
||||
|
||||
.person-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.status-dots {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.navbar-logo {
|
||||
height: 30px;
|
||||
width: auto;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
#sidebar-toggler i {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
/* Make sidebar link text visible */
|
||||
.nav-link-text {
|
||||
display: inline;
|
||||
}
|
||||
253
assets/js/main.js
Normal file
253
assets/js/main.js
Normal file
@ -0,0 +1,253 @@
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const sidebarToggler = document.getElementById('sidebar-toggler');
|
||||
|
||||
if (sidebarToggler) {
|
||||
sidebarToggler.addEventListener('click', function () {
|
||||
const isCollapsed = document.body.classList.toggle('sidebar-collapsed');
|
||||
localStorage.setItem('sidebarCollapsed', isCollapsed);
|
||||
sidebarToggler.setAttribute('aria-expanded', !isCollapsed);
|
||||
});
|
||||
|
||||
// Set initial aria-expanded state
|
||||
const isInitiallyCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
|
||||
sidebarToggler.setAttribute('aria-expanded', !isInitiallyCollapsed);
|
||||
}
|
||||
|
||||
// --- The rest of your original main.js --- //
|
||||
|
||||
$(document).ready(function() {
|
||||
// Handler for showing the edit person modal
|
||||
$('#editPersonModal').on('show.bs.modal', function (event) {
|
||||
var button = $(event.relatedTarget); // Button that triggered the modal
|
||||
var personId = button.data('person-id'); // Extract info from data-* attributes
|
||||
var modal = $(this);
|
||||
|
||||
// Clear previous data
|
||||
modal.find('form').trigger('reset');
|
||||
modal.find('#editPersonId').val('');
|
||||
modal.find('#editRoles').empty();
|
||||
modal.find('#followUpSummaryContainer').empty(); // Clear summary container
|
||||
// Clear file paths
|
||||
modal.find('#editCompanyLogoPath, #editPersonPhotoPath, #editGainsSheetPath, #editTopWantedPath, #editTopOwnedPath').text('');
|
||||
|
||||
if (personId) {
|
||||
// AJAX request to get person details
|
||||
$.ajax({
|
||||
url: '_get_person_details.php',
|
||||
type: 'GET',
|
||||
data: { id: personId },
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.error) {
|
||||
alert('Error fetching person details: ' + response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
var person = response.person;
|
||||
var all_functions = response.all_functions;
|
||||
var person_functions = response.person_functions;
|
||||
var followUpSummary = response.follow_up_summary;
|
||||
|
||||
if (!person) {
|
||||
alert('Could not find person data.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate the Follow-up Summary
|
||||
var summaryContainer = modal.find('#followUpSummaryContainer');
|
||||
if (followUpSummary) {
|
||||
let summaryHtml = '<h5>Follow-up Process Summary</h5>';
|
||||
summaryHtml += '<dl class="row">';
|
||||
|
||||
if (followUpSummary.last_call_outcome) {
|
||||
summaryHtml += `<dt class="col-sm-4">Last Call Outcome</dt><dd class="col-sm-8">${followUpSummary.last_call_outcome.replace(/_/g, ' ')}</dd>`;
|
||||
}
|
||||
if (followUpSummary.last_call_date) {
|
||||
summaryHtml += `<dt class="col-sm-4">Last Call Date</dt><dd class="col-sm-8">${new Date(followUpSummary.last_call_date).toLocaleString()}</dd>`;
|
||||
}
|
||||
if (followUpSummary.next_contact_date) {
|
||||
summaryHtml += `<dt class="col-sm-4">Next Contact Date</dt><dd class="col-sm-8">${new Date(followUpSummary.next_contact_date).toLocaleString()}</dd>`;
|
||||
}
|
||||
if (followUpSummary.final_outcome) {
|
||||
summaryHtml += `<dt class="col-sm-4">Final Status</dt><dd class="col-sm-8">${followUpSummary.final_outcome} (${followUpSummary.reason || 'N/A'})</dd>`;
|
||||
}
|
||||
|
||||
summaryHtml += '</dl>';
|
||||
summaryContainer.html(summaryHtml);
|
||||
} else {
|
||||
summaryContainer.html('<p class="text-muted">No Follow-up process data found for this person.</p>');
|
||||
}
|
||||
|
||||
// Populate the form fields
|
||||
modal.find('#editPersonId').val(person.id);
|
||||
modal.find('#editFirstName').val(person.first_name);
|
||||
modal.find('#editLastName').val(person.last_name);
|
||||
modal.find('#editPhone').val(person.phone);
|
||||
modal.find('#editEmail').val(person.email);
|
||||
modal.find('#editRole').val(person.role);
|
||||
modal.find('#editBniGroup').val(person.bni_group_id);
|
||||
modal.find('#editCompanyName').val(person.company_name);
|
||||
modal.find('#editNip').val(person.nip);
|
||||
modal.find('#editIndustry').val(person.industry);
|
||||
modal.find('#editCompanySize').val(person.company_size_revenue);
|
||||
modal.find('#editBusinessDescription').val(person.business_description);
|
||||
|
||||
// Populate file paths
|
||||
if (person.company_logo_path) {
|
||||
modal.find('#editCompanyLogoPath').text('Current file: ' + person.company_logo_path.split('/').pop());
|
||||
}
|
||||
if (person.person_photo_path) {
|
||||
modal.find('#editPersonPhotoPath').text('Current file: ' + person.person_photo_path.split('/').pop());
|
||||
}
|
||||
if (person.gains_sheet_path) {
|
||||
modal.find('#editGainsSheetPath').text('Current file: ' + person.gains_sheet_path.split('/').pop());
|
||||
}
|
||||
if (person.top_wanted_contacts_path) {
|
||||
modal.find('#editTopWantedPath').text('Current file: ' + person.top_wanted_contacts_path.split('/').pop());
|
||||
}
|
||||
if (person.top_owned_contacts_path) {
|
||||
modal.find('#editTopOwnedPath').text('Current file: ' + person.top_owned_contacts_path.split('/').pop());
|
||||
}
|
||||
|
||||
// Populate functions/roles dropdown and select assigned ones
|
||||
var rolesSelect = modal.find('#editRoles');
|
||||
rolesSelect.empty(); // Clear existing options
|
||||
|
||||
if (all_functions && all_functions.length > 0) {
|
||||
const groupedFunctions = all_functions.reduce((acc, func) => {
|
||||
const groupName = func.group_name || 'General';
|
||||
if (!acc[groupName]) {
|
||||
acc[groupName] = [];
|
||||
}
|
||||
acc[groupName].push(func);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
for (const groupName in groupedFunctions) {
|
||||
const optgroup = $('<optgroup>').attr('label', groupName);
|
||||
groupedFunctions[groupName].forEach(function(func) {
|
||||
var option = $('<option></option>').val(func.id).text(func.name);
|
||||
if (person_functions && person_functions.includes(String(func.id))) {
|
||||
option.prop('selected', true);
|
||||
}
|
||||
optgroup.append(option);
|
||||
});
|
||||
rolesSelect.append(optgroup);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger change to show/hide conditional fields
|
||||
modal.find('#editRole').trigger('change');
|
||||
|
||||
// Also set up the delete button
|
||||
$('#deleteUserBtn').data('person-id', person.id);
|
||||
$('#personNameToDelete').text(person.firstName + ' ' + person.lastName);
|
||||
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
alert('An error occurred while fetching person data. Please try again.');
|
||||
console.error("AJAX Error:", status, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Show/hide group selection based on role for both Edit and Create modals
|
||||
$(document).on('change', '#editRole, #createRole', function() {
|
||||
const role = $(this).val();
|
||||
const isMember = role === 'member';
|
||||
|
||||
// Find the correct context (modal) for the elements
|
||||
const modal = $(this).closest('.modal-content');
|
||||
|
||||
modal.find('.member-only-fields').toggle(isMember);
|
||||
modal.find('#edit-group-selection-div, #create-group-selection-div').toggle(isMember);
|
||||
});
|
||||
|
||||
// Handle Delete Person confirmation
|
||||
$('#confirmDeleteBtn').on('click', function() {
|
||||
var personId = $('#deleteUserBtn').data('person-id');
|
||||
if (personId) {
|
||||
// Use a form submission to perform the delete
|
||||
var form = $('<form></form>');
|
||||
form.attr("method", "post");
|
||||
form.attr("action", "_delete_person.php");
|
||||
|
||||
var field = $('<input></input>');
|
||||
field.attr("type", "hidden");
|
||||
field.attr("name", "id");
|
||||
field.attr("value", personId);
|
||||
form.append(field);
|
||||
|
||||
$(document.body).append(form);
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
|
||||
// Set initial state for create form
|
||||
$('#createPersonModal').on('show.bs.modal', function () {
|
||||
$('#createRole').trigger('change');
|
||||
});
|
||||
|
||||
function handleFormSubmit(form, errorContainer, successCallback) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(form);
|
||||
const errorDiv = $(errorContainer).hide();
|
||||
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
// Clone the response so we can read it twice (once as JSON, once as text if needed)
|
||||
const responseClone = response.clone();
|
||||
return response.json()
|
||||
.then(data => ({ status: response.status, ok: response.ok, body: data }))
|
||||
.catch(() => responseClone.text().then(text => ({ status: response.status, ok: response.ok, body: text, isText: true })));
|
||||
})
|
||||
.then(res => {
|
||||
const { status, ok, body, isText } = res;
|
||||
|
||||
if (!ok) {
|
||||
if (isText) {
|
||||
throw new Error(`Server Error: ${status}. Response: ${body}`);
|
||||
}
|
||||
throw new Error(body.error?.message || `An unknown server error occurred (Status: ${status})`);
|
||||
}
|
||||
|
||||
if (isText) {
|
||||
console.error("Received non-JSON response:", body);
|
||||
throw new Error("The server sent an invalid response that could not be parsed. See console for details.");
|
||||
}
|
||||
|
||||
if (body.success) {
|
||||
if (successCallback) {
|
||||
successCallback(body);
|
||||
} else {
|
||||
// Default success behavior: close modal and reload
|
||||
$(form).closest('.modal').modal('hide');
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
throw new Error(body.error?.message || 'An operation error occurred.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
errorDiv.text(error.message).show();
|
||||
});
|
||||
}
|
||||
|
||||
$('#createPersonForm').on('submit', function(event) {
|
||||
handleFormSubmit(this, '#createPersonError');
|
||||
});
|
||||
|
||||
$('#editPersonForm').on('submit', function(event) {
|
||||
handleFormSubmit(this, '#editPersonError', function(data) {
|
||||
// close modal and reload page
|
||||
$('#editPersonModal').modal('hide');
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
157
assets/js/process_definitions.js
Normal file
157
assets/js/process_definitions.js
Normal file
@ -0,0 +1,157 @@
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const createProcessModal = document.getElementById('createProcessModal');
|
||||
const modalTitle = createProcessModal.querySelector('.modal-title');
|
||||
const form = createProcessModal.querySelector('#createProcessForm');
|
||||
const processIdInput = createProcessModal.querySelector('#processId');
|
||||
const processNameInput = createProcessModal.querySelector('#processName');
|
||||
const definitionJsonTextarea = createProcessModal.querySelector('#definitionJson');
|
||||
const eligibilityRulesJsonTextarea = createProcessModal.querySelector('#eligibilityRulesJson');
|
||||
|
||||
let definition = {};
|
||||
|
||||
function render() {
|
||||
const statusesList = document.getElementById('statuses-list');
|
||||
const transitionsList = document.getElementById('transitions-list');
|
||||
const initialStatusSelect = document.getElementById('initialStatus');
|
||||
const fromStatusSelect = document.getElementById('fromStatusSelect');
|
||||
const toStatusSelect = document.getElementById('toStatusSelect');
|
||||
|
||||
statusesList.innerHTML = '';
|
||||
transitionsList.innerHTML = '';
|
||||
initialStatusSelect.innerHTML = '';
|
||||
fromStatusSelect.innerHTML = '';
|
||||
toStatusSelect.innerHTML = '';
|
||||
|
||||
if (definition.nodes) {
|
||||
for (const nodeId in definition.nodes) {
|
||||
const node = definition.nodes[nodeId];
|
||||
const statusItem = document.createElement('div');
|
||||
statusItem.textContent = node.name;
|
||||
statusesList.appendChild(statusItem);
|
||||
|
||||
const option = document.createElement('option');
|
||||
option.value = node.id;
|
||||
option.textContent = node.name;
|
||||
initialStatusSelect.appendChild(option.cloneNode(true));
|
||||
fromStatusSelect.appendChild(option.cloneNode(true));
|
||||
toStatusSelect.appendChild(option.cloneNode(true));
|
||||
}
|
||||
}
|
||||
|
||||
if (definition.start_node_id) {
|
||||
initialStatusSelect.value = definition.start_node_id;
|
||||
}
|
||||
|
||||
if (definition.transitions) {
|
||||
definition.transitions.forEach(transition => {
|
||||
const transitionItem = document.createElement('div');
|
||||
const fromNode = definition.nodes[transition.from] ? definition.nodes[transition.from].name : 'N/A';
|
||||
const toNode = definition.nodes[transition.to] ? definition.nodes[transition.to].name : 'N/A';
|
||||
let actions = '';
|
||||
if(transition.actions) {
|
||||
actions = ' - Actions: ' + JSON.stringify(transition.actions);
|
||||
}
|
||||
transitionItem.textContent = `${transition.name}: ${fromNode} => ${toNode}${actions}`;
|
||||
transitionsList.appendChild(transitionItem);
|
||||
});
|
||||
}
|
||||
|
||||
if (definition.eligibility_rules) {
|
||||
eligibilityRulesJsonTextarea.value = JSON.stringify(definition.eligibility_rules, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('addStatusBtn').addEventListener('click', function () {
|
||||
const newStatusInput = document.getElementById('newStatusInput');
|
||||
const newStatusName = newStatusInput.value.trim();
|
||||
if (newStatusName) {
|
||||
const newNodeId = (Object.keys(definition.nodes || {}).length + 1).toString();
|
||||
if (!definition.nodes) {
|
||||
definition.nodes = {};
|
||||
}
|
||||
definition.nodes[newNodeId] = { id: newNodeId, name: newStatusName };
|
||||
newStatusInput.value = '';
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('addTransitionBtn').addEventListener('click', function () {
|
||||
const fromStatus = document.getElementById('fromStatusSelect').value;
|
||||
const toStatus = document.getElementById('toStatusSelect').value;
|
||||
const transitionActionJson = document.getElementById('transitionActionJson').value;
|
||||
|
||||
if (fromStatus && toStatus) {
|
||||
if (!definition.transitions) {
|
||||
definition.transitions = [];
|
||||
}
|
||||
const newTransition = {
|
||||
name: `Transition ${definition.transitions.length + 1}`,
|
||||
from: fromStatus,
|
||||
to: toStatus,
|
||||
};
|
||||
|
||||
if(transitionActionJson) {
|
||||
try {
|
||||
newTransition.actions = JSON.parse(transitionActionJson);
|
||||
} catch(e) {
|
||||
alert('Invalid JSON in transition actions');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
definition.transitions.push(newTransition);
|
||||
document.getElementById('transitionActionJson').value = '';
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener('submit', function (event) {
|
||||
definition.start_node_id = document.getElementById('initialStatus').value;
|
||||
|
||||
try {
|
||||
const eligibilityRules = eligibilityRulesJsonTextarea.value;
|
||||
if(eligibilityRules) {
|
||||
definition.eligibility_rules = JSON.parse(eligibilityRules);
|
||||
} else {
|
||||
delete definition.eligibility_rules;
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Invalid JSON in eligibility rules');
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
definitionJsonTextarea.value = JSON.stringify(definition, null, 2);
|
||||
});
|
||||
|
||||
createProcessModal.addEventListener('show.bs.modal', function (event) {
|
||||
const button = event.relatedTarget;
|
||||
const isEdit = button.classList.contains('edit-process-btn');
|
||||
|
||||
if (isEdit) {
|
||||
const processId = button.dataset.processId;
|
||||
const processName = button.dataset.processName;
|
||||
const processDefinition = button.dataset.processDefinition;
|
||||
|
||||
modalTitle.textContent = 'Edit Process';
|
||||
processIdInput.value = processId;
|
||||
processNameInput.value = processName;
|
||||
|
||||
try {
|
||||
definition = JSON.parse(processDefinition || '{}');
|
||||
} catch(e) {
|
||||
definition = {};
|
||||
}
|
||||
|
||||
} else {
|
||||
modalTitle.textContent = 'Create Process';
|
||||
processIdInput.value = '';
|
||||
processNameInput.value = '';
|
||||
definition = {};
|
||||
}
|
||||
|
||||
eligibilityRulesJsonTextarea.value = '';
|
||||
render();
|
||||
});
|
||||
});
|
||||
BIN
assets/pasted-20260111-143449-befa41d3.png
Normal file
BIN
assets/pasted-20260111-143449-befa41d3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
BIN
assets/pasted-20260111-144117-aba8ec29.jpg
Normal file
BIN
assets/pasted-20260111-144117-aba8ec29.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
201
bni_groups.php
Normal file
201
bni_groups.php
Normal file
@ -0,0 +1,201 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
include '_header.php';
|
||||
include '_navbar.php';
|
||||
|
||||
$pdo = db();
|
||||
$stmt = $pdo->query("SELECT * FROM bni_groups ORDER BY display_order");
|
||||
$bni_groups = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<?php include '_sidebar.php'; ?>
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<h1 class="h2 pt-3 pb-2 mb-3 border-bottom">BNI Groups</h1>
|
||||
|
||||
<?php if (isset($_SESSION['success_message'])): ?>
|
||||
<div class="alert alert-success alert-dismissible fade show mt-3" role="alert">
|
||||
<?= $_SESSION['success_message']; ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php unset($_SESSION['success_message']); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
|
||||
Add New BNI Group
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive mt-4">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 30px;"></th>
|
||||
<th>Name</th>
|
||||
<th>City</th>
|
||||
<th>Active</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sortable-list">
|
||||
<?php foreach ($bni_groups as $group): ?>
|
||||
<tr data-id="<?= $group['id'] ?>">
|
||||
<td class="handle"><i class="bi bi-grip-vertical"></i></td>
|
||||
<td><?= htmlspecialchars($group['name']) ?></td>
|
||||
<td><?= htmlspecialchars($group['city']) ?></td>
|
||||
<td><?= $group['active'] ? 'Yes' : 'No' ?></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-warning btn-sm" data-bs-toggle="modal" data-bs-target="#editModal"
|
||||
data-id="<?= $group['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($group['name']) ?>"
|
||||
data-city="<?= htmlspecialchars($group['city']) ?>"
|
||||
data-active="<?= $group['active'] ?>">
|
||||
Edit
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteModal" data-id="<?= $group['id'] ?>">
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Modal -->
|
||||
<div class="modal fade" id="addModal" tabindex="-1" aria-labelledby="addModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addModalLabel">Add BNI Group</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="_add_bni_group.php" method="post">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="addName" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="addName" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addCity" class="form-label">City</label>
|
||||
<input type="text" class="form-control" id="addCity" name="city">
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" value="1" id="addActive" name="active" checked>
|
||||
<label class="form-check-label" for="addActive">
|
||||
Active
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" name="add" class="btn btn-primary">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editModalLabel">Edit BNI Group</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="_update_bni_group.php" method="post">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="editId" name="id">
|
||||
<div class="mb-3">
|
||||
<label for="editName" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="editName" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editCity" class="form-label">City</label>
|
||||
<input type="text" class="form-control" id="editCity" name="city">
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" value="1" id="editActive" name="active">
|
||||
<label class="form-check-label" for="editActive">
|
||||
Active
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" name="edit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">Delete BNI Group</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to delete this BNI group?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<a href="#" id="deleteLink" class="btn btn-danger">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<?php include '_footer.php'; ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var editModal = document.getElementById('editModal');
|
||||
editModal.addEventListener('show.bs.modal', function (event) {
|
||||
var button = event.relatedTarget;
|
||||
var id = button.getAttribute('data-id');
|
||||
var name = button.getAttribute('data-name');
|
||||
var city = button.getAttribute('data-city');
|
||||
var active = button.getAttribute('data-active');
|
||||
|
||||
var idInput = editModal.querySelector('#editId');
|
||||
var nameInput = editModal.querySelector('#editName');
|
||||
var cityInput = editModal.querySelector('#editCity');
|
||||
var activeInput = editModal.querySelector('#editActive');
|
||||
|
||||
idInput.value = id;
|
||||
nameInput.value = name;
|
||||
cityInput.value = city;
|
||||
activeInput.checked = (active == 1);
|
||||
});
|
||||
|
||||
var deleteModal = document.getElementById('deleteModal');
|
||||
deleteModal.addEventListener('show.bs.modal', function (event) {
|
||||
var button = event.relatedTarget;
|
||||
var id = button.getAttribute('data-id');
|
||||
var deleteLink = deleteModal.querySelector('#deleteLink');
|
||||
deleteLink.href = '_delete_bni_group.php?id=' + id;
|
||||
});
|
||||
|
||||
$(function() {
|
||||
$("#sortable-list").sortable({
|
||||
handle: ".handle",
|
||||
update: function(event, ui) {
|
||||
var order = $(this).sortable('toArray', {attribute: 'data-id'});
|
||||
$.post('_update_bni_group_order.php', { order: order });
|
||||
}
|
||||
}).disableSelection();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
352
calendar.php
Normal file
352
calendar.php
Normal file
@ -0,0 +1,352 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
include '_header.php';
|
||||
|
||||
// Get current month and year
|
||||
$month = isset($_GET['month']) ? (int)$_GET['month'] : date('m');
|
||||
$year = isset($_GET['year']) ? (int)$_GET['year'] : date('Y');
|
||||
|
||||
// Create a DateTime object for the first day of the month
|
||||
$firstDayOfMonth = new DateTime("$year-$month-01");
|
||||
$daysInMonth = $firstDayOfMonth->format('t');
|
||||
$dayOfWeek = $firstDayOfMonth->format('N'); // 1 (for Monday) through 7 (for Sunday)
|
||||
|
||||
// Get events for the current month
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("SELECT c.*, t.name as type_name, t.color as type_color, GROUP_CONCAT(g.name SEPARATOR ', ') as group_names FROM calendar_events c LEFT JOIN event_types t ON c.event_type_id = t.id LEFT JOIN calendar_event_groups ceg ON c.id = ceg.calendar_event_id LEFT JOIN bni_groups g ON ceg.bni_group_id = g.id WHERE MONTH(c.start_datetime) = ? AND YEAR(c.start_datetime) = ? GROUP BY c.id ORDER BY c.start_datetime ASC");
|
||||
$stmt->execute([$month, $year]);
|
||||
$events = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$eventsByDay = [];
|
||||
foreach ($events as $event) {
|
||||
$day = (new DateTime($event['start_datetime']))->format('j');
|
||||
if (!isset($eventsByDay[$day])) {
|
||||
$eventsByDay[$day] = [];
|
||||
}
|
||||
$eventsByDay[$day][] = $event;
|
||||
}
|
||||
|
||||
// Get event types for the modal
|
||||
$stmt_types = $pdo->query("SELECT * FROM event_types ORDER BY display_order");
|
||||
$event_types = $stmt_types->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Get BNI groups for the modal
|
||||
$stmt_groups = $pdo->query("SELECT * FROM bni_groups ORDER BY display_order");
|
||||
$bni_groups = $stmt_groups->fetchAll(PDO::FETCH_ASSOC);
|
||||
$prevMonth = $month == 1 ? 12 : $month - 1;
|
||||
$prevYear = $month == 1 ? $year - 1 : $year;
|
||||
$nextMonth = $month == 12 ? 1 : $month + 1;
|
||||
$nextYear = $month == 12 ? $year + 1 : $year;
|
||||
|
||||
?>
|
||||
|
||||
|
||||
<?php include '_navbar.php'; ?>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<?php include '_sidebar.php'; ?>
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
|
||||
<h2 class="text-center"><?php echo $firstDayOfMonth->format('F Y'); ?></h2>
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<a href="?month=<?php echo $prevMonth; ?>&year=<?php echo $prevYear; ?>" class="btn btn-primary">< Previous</a>
|
||||
<a href="?month=<?php echo $nextMonth; ?>&year=<?php echo $nextYear; ?>" class="btn btn-primary">Next ></a>
|
||||
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addEventModal">
|
||||
Add Event
|
||||
</button>
|
||||
</div>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Monday</th>
|
||||
<th>Tuesday</th>
|
||||
<th>Wednesday</th>
|
||||
<th>Thursday</th>
|
||||
<th>Friday</th>
|
||||
<th>Saturday</th>
|
||||
<th>Sunday</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<?php
|
||||
// Print empty cells
|
||||
for ($i = 1; $i < $dayOfWeek; $i++) {
|
||||
echo "<td></td>";
|
||||
}
|
||||
|
||||
$currentDay = 1;
|
||||
while ($currentDay <= $daysInMonth) {
|
||||
if ($dayOfWeek > 7) {
|
||||
$dayOfWeek = 1;
|
||||
echo "</tr><tr>";
|
||||
}
|
||||
|
||||
echo "<td class=\"calendar-day\" data-date=\"$year-$month-$currentDay\">";
|
||||
echo "<strong>$currentDay</strong>";
|
||||
if (isset($eventsByDay[$currentDay])) {
|
||||
echo "<ul class=\"list-unstyled\">";
|
||||
foreach ($eventsByDay[$currentDay] as $event) {
|
||||
echo '<li class="badge" style="background-color: '.($event['type_color'] ?? '#007bff').'" data-event-id="'.$event['id'].'">' . htmlspecialchars($event['title']);
|
||||
if (!empty($event['group_names'])) {
|
||||
echo '<br><small class="fst-italic">(' . htmlspecialchars($event['group_names']) . ')</small>';
|
||||
}
|
||||
echo "</li>";
|
||||
}
|
||||
echo "</ul>";
|
||||
}
|
||||
echo "</td>";
|
||||
|
||||
$currentDay++;
|
||||
$dayOfWeek++;
|
||||
}
|
||||
|
||||
// Print remaining empty cells
|
||||
while ($dayOfWeek <= 7) {
|
||||
echo "<td></td>";
|
||||
$dayOfWeek++;
|
||||
}
|
||||
?>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Details Modal -->
|
||||
<div class="modal fade" id="eventDetailsModal" tabindex="-1" aria-labelledby="eventDetailsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="eventDetailsModalLabel">Event Details</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5 id="event-title"></h5>
|
||||
<p id="event-description"></p>
|
||||
<p><strong>Starts:</strong> <span id="event-start"></span></p>
|
||||
<p><strong>Ends:</strong> <span id="event-end"></span></p>
|
||||
<p><strong>Type:</strong> <span id="event-type"></span></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" id="edit-event-btn">Edit</button>
|
||||
<a href="#" id="delete-event-btn" class="btn btn-danger">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="addEventModal" tabindex="-1" aria-labelledby="addEventModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addEventModalLabel">Add Calendar Event</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="_add_calendar_event.php" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="title" name="title" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="start_datetime" class="form-label">Start Time</label>
|
||||
<input type="datetime-local" class="form-control" id="start_datetime" name="start_datetime" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="end_datetime" class="form-label">End Time</label>
|
||||
<input type="datetime-local" class="form-control" id="end_datetime" name="end_datetime" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="event_type_id" class="form-label">Type</label>
|
||||
<select class="form-select" id="event_type_id" name="event_type_id">
|
||||
<?php foreach ($event_types as $type): ?>
|
||||
<option value="<?= $type['id'] ?>"><?= htmlspecialchars($type['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="group_ids" class="form-label">Group(s)</label>
|
||||
<select class="form-select" id="group_ids" name="group_ids[]" multiple required>
|
||||
<?php foreach ($bni_groups as $group): ?>
|
||||
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="recurrence" class="form-label">Recurrence</label>
|
||||
<select class="form-select" id="recurrence" name="recurrence">
|
||||
<option value="">Does not repeat</option>
|
||||
<option value="daily">Daily</option>
|
||||
<option value="weekly">Weekly</option>
|
||||
<option value="monthly">Monthly</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3" id="recurrence_end_date_container" style="display: none;">
|
||||
<label for="recurrence_end_date" class="form-label">Recurrence End Date</label>
|
||||
<input type="date" class="form-control" id="recurrence_end_date" name="recurrence_end_date">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save Event</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="editEventModal" tabindex="-1" aria-labelledby="editEventModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editEventModalLabel">Edit Calendar Event</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="_update_calendar_event.php" method="post">
|
||||
<input type="hidden" id="edit_event_id" name="event_id">
|
||||
<div class="mb-3">
|
||||
<label for="edit_title" class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="edit_title" name="title" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="edit_description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="edit_description" name="description"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="edit_start_datetime" class="form-label">Start Time</label>
|
||||
<input type="datetime-local" class="form-control" id="edit_start_datetime" name="start_datetime" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="edit_end_datetime" class="form-label">End Time</label>
|
||||
<input type="datetime-local" class="form-control" id="edit_end_datetime" name="end_datetime" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="edit_event_type_id" class="form-label">Type</label>
|
||||
<select class="form-select" id="edit_event_type_id" name="event_type_id">
|
||||
<?php foreach ($event_types as $type): ?>
|
||||
<option value="<?= $type['id'] ?>"><?= htmlspecialchars($type['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="edit_group_ids" class="form-label">Group(s)</label>
|
||||
<select class="form-select" id="edit_group_ids" name="group_ids[]" multiple required>
|
||||
<?php foreach ($bni_groups as $group): ?>
|
||||
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div id="edit-recurrence-options" class="mb-3" style="display: none;">
|
||||
<strong>This is a recurring event.</strong><br>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="update_scope" id="update_scope_one" value="one" checked>
|
||||
<label class="form-check-label" for="update_scope_one">
|
||||
Update this event only
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="update_scope" id="update_scope_all" value="all">
|
||||
<label class="form-check-label" for="update_scope_all">
|
||||
Update all events in the series
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const addEventModal = new bootstrap.Modal(document.getElementById('addEventModal'));
|
||||
const eventDetailsModal = new bootstrap.Modal(document.getElementById('eventDetailsModal'));
|
||||
const editEventModal = new bootstrap.Modal(document.getElementById('editEventModal'));
|
||||
|
||||
let currentEventData = null;
|
||||
|
||||
document.querySelectorAll('.calendar-day').forEach(day => {
|
||||
day.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('badge')) {
|
||||
const eventId = e.target.dataset.eventId;
|
||||
fetch(`_get_event_details.php?id=${eventId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
currentEventData = data;
|
||||
document.getElementById('event-title').textContent = data.title;
|
||||
document.getElementById('event-description').textContent = data.description;
|
||||
document.getElementById('event-start').textContent = new Date(data.start_datetime).toLocaleString();
|
||||
document.getElementById('event-end').textContent = new Date(data.end_datetime).toLocaleString();
|
||||
document.getElementById('event-type').textContent = data.type_name;
|
||||
document.getElementById('delete-event-btn').href = `_delete_calendar_event.php?id=${eventId}`;
|
||||
eventDetailsModal.show();
|
||||
});
|
||||
} else {
|
||||
const date = this.dataset.date;
|
||||
const now = new Date();
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const currentTime = `${hours}:${minutes}`;
|
||||
|
||||
const startInput = document.getElementById('start_datetime');
|
||||
const month = String(new Date(date).getMonth() + 1).padStart(2, '0');
|
||||
const dayOfMonth = String(new Date(date).getDate()).padStart(2, '0');
|
||||
const year = new Date(date).getFullYear();
|
||||
startInput.value = `${year}-${month}-${dayOfMonth}T${currentTime}`;
|
||||
|
||||
addEventModal.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('edit-event-btn').addEventListener('click', function() {
|
||||
if (currentEventData) {
|
||||
const recurrenceOptions = document.getElementById('edit-recurrence-options');
|
||||
if (currentEventData.parent_event_id !== null || currentEventData.recurrence !== null) {
|
||||
recurrenceOptions.style.display = 'block';
|
||||
document.getElementById('update_scope_all').checked = true;
|
||||
} else {
|
||||
recurrenceOptions.style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('edit_event_id').value = currentEventData.id;
|
||||
document.getElementById('edit_title').value = currentEventData.title;
|
||||
document.getElementById('edit_description').value = currentEventData.description;
|
||||
document.getElementById('edit_start_datetime').value = currentEventData.start_datetime.slice(0, 16);
|
||||
document.getElementById('edit_end_datetime').value = currentEventData.end_datetime.slice(0, 16);
|
||||
document.getElementById('edit_event_type_id').value = currentEventData.event_type_id;
|
||||
|
||||
const groupSelect = document.getElementById('edit_group_ids');
|
||||
const groupIds = currentEventData.group_ids || [];
|
||||
for (const option of groupSelect.options) {
|
||||
option.selected = groupIds.includes(option.value);
|
||||
}
|
||||
|
||||
eventDetailsModal.hide();
|
||||
editEventModal.show();
|
||||
}
|
||||
});
|
||||
|
||||
const recurrenceSelect = document.getElementById('recurrence');
|
||||
const recurrenceEndDateContainer = document.getElementById('recurrence_end_date_container');
|
||||
|
||||
recurrenceSelect.addEventListener('change', function() {
|
||||
if (this.value) {
|
||||
recurrenceEndDateContainer.style.display = 'block';
|
||||
} else {
|
||||
recurrenceEndDateContainer.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
include '_footer.php';
|
||||
?>
|
||||
5
cookie.txt
Normal file
5
cookie.txt
Normal file
@ -0,0 +1,5 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
localhost FALSE / FALSE 0 PHPSESSID 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" } ]
|
||||
}
|
||||
]
|
||||
}
|
||||
17
db/config.php
Normal file
17
db/config.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
// Generated by setup_mariadb_project.sh — edit as needed.
|
||||
define('DB_HOST', '127.0.0.1');
|
||||
define('DB_NAME', 'app_37338');
|
||||
define('DB_USER', 'app_37338');
|
||||
define('DB_PASS', '70d73026-c3ce-47ed-80b9-28f8c3bf35dd');
|
||||
|
||||
function db() {
|
||||
static $pdo;
|
||||
if (!$pdo) {
|
||||
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
8
db/migrations/001_add_process_definition_json.php
Normal file
8
db/migrations/001_add_process_definition_json.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
function migrate_001($pdo) {
|
||||
$sql = "ALTER TABLE process_definitions ADD COLUMN definition_json TEXT NULL AFTER name;";
|
||||
$pdo->exec($sql);
|
||||
echo "Migration 001 applied: Added definition_json to process_definitions.\n";
|
||||
}
|
||||
|
||||
17
db/migrations/002_create_calendar_events_table.php
Normal file
17
db/migrations/002_create_calendar_events_table.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
function migrate_002($pdo) {
|
||||
$sql = "CREATE TABLE IF NOT EXISTS calendar_events (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
start_datetime DATETIME NOT NULL,
|
||||
end_datetime DATETIME NOT NULL,
|
||||
type VARCHAR(50) NOT NULL, -- 'meeting', 'training'
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);";
|
||||
$pdo->exec($sql);
|
||||
echo "Migration 002 applied: Created calendar_events table.\n";
|
||||
}
|
||||
|
||||
|
||||
26
db/migrations/003_create_event_types_table.php
Normal file
26
db/migrations/003_create_event_types_table.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS event_types (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
color VARCHAR(7) NOT NULL DEFAULT '#007bff'
|
||||
);";
|
||||
|
||||
$pdo->exec($sql);
|
||||
|
||||
// Add some default values
|
||||
$sql_insert = "INSERT INTO event_types (name, color) VALUES
|
||||
('Meeting', '#007bff'),
|
||||
('Training', '#ffc107'),
|
||||
('Other', '#28a745');";
|
||||
$pdo->exec($sql_insert);
|
||||
|
||||
|
||||
echo "Migration 003 completed successfully.\n";
|
||||
} catch (PDOException $e) {
|
||||
die("Migration 003 failed: " . $e->getMessage() . "\n");
|
||||
}
|
||||
34
db/migrations/004_add_event_type_id_to_calendar_events.php
Normal file
34
db/migrations/004_add_event_type_id_to_calendar_events.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
// First, add the column without the foreign key constraint
|
||||
$sql_add_column = "ALTER TABLE calendar_events ADD COLUMN event_type_id INT NULL;";
|
||||
$pdo->exec($sql_add_column);
|
||||
|
||||
// Set a default value for existing rows to avoid foreign key constraint errors
|
||||
// Get the ID of the 'Other' event type
|
||||
$stmt = $pdo->query("SELECT id FROM event_types WHERE name = 'Other' LIMIT 1");
|
||||
$other_type = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$default_type_id = $other_type ? $other_type['id'] : 1; // Fallback to 1 if 'Other' not found
|
||||
|
||||
$sql_update_existing = "UPDATE calendar_events SET event_type_id = ? WHERE event_type_id IS NULL;";
|
||||
$stmt_update = $pdo->prepare($sql_update_existing);
|
||||
$stmt_update->execute([$default_type_id]);
|
||||
|
||||
// Now, add the foreign key constraint
|
||||
$sql_add_fk = "ALTER TABLE calendar_events ADD CONSTRAINT fk_event_type FOREIGN KEY (event_type_id) REFERENCES event_types(id) ON DELETE SET NULL;";
|
||||
$pdo->exec($sql_add_fk);
|
||||
|
||||
echo "Migration 004 completed successfully.\n";
|
||||
} catch (PDOException $e) {
|
||||
// Check if the column already exists, which might happen on re-runs
|
||||
if (strpos($e->getMessage(), 'Duplicate column name') !== false) {
|
||||
echo "Migration 004 seems to be already applied (Column exists). Skipping.\n";
|
||||
} else {
|
||||
die("Migration 004 failed: " . $e->getMessage() . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
18
db/migrations/005_create_bni_groups_table.php
Normal file
18
db/migrations/005_create_bni_groups_table.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../config.php');
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$sql = "CREATE TABLE IF NOT EXISTS `bni_groups` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`city` VARCHAR(255),
|
||||
`active` BOOLEAN NOT NULL DEFAULT 1,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);";
|
||||
$pdo->exec($sql);
|
||||
echo "Table 'bni_groups' created successfully." . PHP_EOL;
|
||||
} catch (PDOException $e) {
|
||||
echo "Error creating table 'bni_groups': " . $e->getMessage() . PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
20
db/migrations/006_add_recurrence_to_calendar_events.php
Normal file
20
db/migrations/006_add_recurrence_to_calendar_events.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
$sql = <<<SQL
|
||||
ALTER TABLE `calendar_events`
|
||||
ADD COLUMN `recurrence` VARCHAR(20) DEFAULT NULL,
|
||||
ADD COLUMN `recurrence_end_date` DATE DEFAULT NULL,
|
||||
ADD COLUMN `parent_event_id` INT DEFAULT NULL;
|
||||
SQL;
|
||||
|
||||
$pdo->exec($sql);
|
||||
|
||||
echo "Migration 006 executed successfully: Added recurrence columns to calendar_events table.\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
die("Error executing migration 006: " . $e->getMessage() . "\n");
|
||||
}
|
||||
17
db/migrations/007_remove_type_from_calendar_events.php
Normal file
17
db/migrations/007_remove_type_from_calendar_events.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$sql = "ALTER TABLE calendar_events DROP COLUMN type;";
|
||||
$pdo->exec($sql);
|
||||
echo "Migration 007 completed successfully: Dropped 'type' column from calendar_events.\n";
|
||||
} catch (PDOException $e) {
|
||||
// Check if the column has already been dropped
|
||||
if (strpos($e->getMessage(), 'column not found') !== false || strpos($e->getMessage(), 'Unknown column') !== false) {
|
||||
echo "Migration 007 seems to be already applied (Column not found). Skipping.\n";
|
||||
} else {
|
||||
die("Migration 007 failed: " . $e->getMessage() . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
16
db/migrations/008_create_roles_table.php
Normal file
16
db/migrations/008_create_roles_table.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../config.php');
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$sql = "CREATE TABLE IF NOT EXISTS `roles` (
|
||||
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
|
||||
$pdo->exec($sql);
|
||||
echo "Table 'roles' created successfully." . PHP_EOL;
|
||||
} catch (PDOException $e) {
|
||||
echo "Error creating table 'roles': " . $e->getMessage() . PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
18
db/migrations/009_create_user_roles_table.php
Normal file
18
db/migrations/009_create_user_roles_table.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../config.php');
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$sql = "CREATE TABLE IF NOT EXISTS `user_roles` (
|
||||
`user_id` INT(11) UNSIGNED NOT NULL,
|
||||
`role_id` INT(11) UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (`user_id`, `role_id`),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `people`(`id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
|
||||
$pdo->exec($sql);
|
||||
echo "Table 'user_roles' created successfully." . PHP_EOL;
|
||||
} catch (PDOException $e) {
|
||||
echo "Error creating table 'user_roles': " . $e->getMessage() . PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
12
db/migrations/010_add_display_order_to_event_types.php
Normal file
12
db/migrations/010_add_display_order_to_event_types.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$sql = "ALTER TABLE event_types ADD COLUMN display_order INT NOT NULL DEFAULT 0";
|
||||
$pdo->exec($sql);
|
||||
echo "Migration 010_add_display_order_to_event_types executed successfully." . PHP_EOL;
|
||||
} catch (PDOException $e) {
|
||||
echo "Error executing migration 010_add_display_order_to_event_types: " . $e->getMessage() . PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
12
db/migrations/011_add_display_order_to_bni_groups.php
Normal file
12
db/migrations/011_add_display_order_to_bni_groups.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$sql = "ALTER TABLE bni_groups ADD COLUMN display_order INT NOT NULL DEFAULT 0";
|
||||
$pdo->exec($sql);
|
||||
echo "Migration 011_add_display_order_to_bni_groups executed successfully." . PHP_EOL;
|
||||
} catch (PDOException $e) {
|
||||
echo "Error executing migration 011_add_display_order_to_bni_groups: " . $e->getMessage() . PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
12
db/migrations/012_add_display_order_to_roles.php
Normal file
12
db/migrations/012_add_display_order_to_roles.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$sql = "ALTER TABLE roles ADD COLUMN display_order INT NOT NULL DEFAULT 0";
|
||||
$pdo->exec($sql);
|
||||
echo "Migration 012_add_display_order_to_roles executed successfully." . PHP_EOL;
|
||||
} catch (PDOException $e) {
|
||||
echo "Error executing migration 012_add_display_order_to_roles: " . $e->getMessage() . PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
21
db/migrations/013_rename_roles_to_functions.php
Normal file
21
db/migrations/013_rename_roles_to_functions.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../config.php');
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$sql = "RENAME TABLE `roles` TO `functions`;";
|
||||
$pdo->exec($sql);
|
||||
echo "Table 'roles' renamed to 'functions' successfully." . PHP_EOL;
|
||||
|
||||
$sql = "RENAME TABLE `user_roles` TO `user_functions`;";
|
||||
$pdo->exec($sql);
|
||||
echo "Table 'user_roles' renamed to 'user_functions' successfully." . PHP_EOL;
|
||||
|
||||
$sql = "ALTER TABLE `user_functions` CHANGE `role_id` `function_id` INT(11) UNSIGNED NOT NULL;";
|
||||
$pdo->exec($sql);
|
||||
echo "Column 'role_id' renamed to 'function_id' in 'user_functions' table successfully." . PHP_EOL;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
echo "Error renaming tables or columns: " . $e->getMessage() . PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
21
db/migrations/014_add_role_to_people_table.php
Normal file
21
db/migrations/014_add_role_to_people_table.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
function migrate_014_add_role_to_people_table() {
|
||||
$pdo = db();
|
||||
|
||||
// Check if the column already exists
|
||||
try {
|
||||
$result = $pdo->query("SELECT `role` FROM `people` LIMIT 1");
|
||||
} catch (PDOException $e) {
|
||||
// Column does not exist, so add it
|
||||
$pdo->exec("ALTER TABLE `people` ADD COLUMN `role` VARCHAR(50) NOT NULL DEFAULT 'członek' AFTER `email`");
|
||||
echo "Migration 014: Added 'role' column to 'people' table.\n";
|
||||
return;
|
||||
}
|
||||
|
||||
echo "Migration 014: 'role' column already exists in 'people' table. No changes made.\n";
|
||||
}
|
||||
|
||||
migrate_014_add_role_to_people_table();
|
||||
|
||||
61
db/migrations/015_add_group_id_to_functions.php
Normal file
61
db/migrations/015_add_group_id_to_functions.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
// First, check if the column already exists
|
||||
$stmt = $pdo->query("SHOW COLUMNS FROM `functions` LIKE 'bni_group_id'");
|
||||
$exists = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// Check if the foreign key constraint already exists
|
||||
$stmt = $pdo->query("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'functions' AND CONSTRAINT_NAME = 'fk_functions_bni_group'");
|
||||
$fk_exists = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($fk_exists) {
|
||||
echo "Migration 015 skipped: Foreign key 'fk_functions_bni_group' already exists." . PHP_EOL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$exists) {
|
||||
// Add the bni_group_id column allowing NULLs temporarily
|
||||
$pdo->exec("ALTER TABLE `functions` ADD COLUMN `bni_group_id` INT NULL AFTER `id`;");
|
||||
echo "Column 'bni_group_id' added." . PHP_EOL;
|
||||
}
|
||||
|
||||
// Find a default group to assign to existing functions
|
||||
$stmt = $pdo->query("SELECT id FROM `bni_groups` ORDER BY id LIMIT 1");
|
||||
$default_group = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$default_group_id = $default_group ? $default_group['id'] : null;
|
||||
|
||||
if ($default_group_id) {
|
||||
// Update existing functions to use the default group where bni_group_id is not valid
|
||||
$pdo->exec("UPDATE `functions` SET `bni_group_id` = {$default_group_id} WHERE `bni_group_id` IS NULL OR `bni_group_id` NOT IN (SELECT id FROM bni_groups)");
|
||||
echo "Existing functions updated with a valid group ID." . PHP_EOL;
|
||||
|
||||
// Now that existing rows are updated, alter the column to be NOT NULL
|
||||
$pdo->exec("ALTER TABLE `functions` MODIFY COLUMN `bni_group_id` INT NOT NULL;");
|
||||
echo "Column 'bni_group_id' modified to NOT NULL." . PHP_EOL;
|
||||
|
||||
// Add the foreign key constraint
|
||||
$pdo->exec("ALTER TABLE `functions` ADD CONSTRAINT `fk_functions_bni_group` FOREIGN KEY (`bni_group_id`) REFERENCES `bni_groups`(`id`) ON DELETE CASCADE;");
|
||||
echo "Migration 015 successfully applied." . PHP_EOL;
|
||||
|
||||
} else {
|
||||
// If there are no groups, we can't proceed if there are functions.
|
||||
$stmt = $pdo->query("SELECT COUNT(*) FROM `functions`");
|
||||
$function_count = $stmt->fetchColumn();
|
||||
if ($function_count > 0) {
|
||||
die("Migration 015 failed: Cannot create a required association to a BNI group because no BNI groups exist, but functions that need them do exist." . PHP_EOL);
|
||||
} else {
|
||||
// No functions exist, so we can just add the column and constraint
|
||||
$pdo->exec("ALTER TABLE `functions` MODIFY COLUMN `bni_group_id` INT NOT NULL;");
|
||||
$pdo->exec("ALTER TABLE `functions` ADD CONSTRAINT `fk_functions_bni_group` FOREIGN KEY (`bni_group_id`) REFERENCES `bni_groups`(`id`) ON DELETE CASCADE;");
|
||||
echo "Migration 015 successfully applied (no existing functions to update)." . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch (PDOException $e) {
|
||||
die("Migration 015 failed: " . $e->getMessage() . PHP_EOL);
|
||||
}
|
||||
30
db/migrations/016_create_calendar_event_groups_table.php
Normal file
30
db/migrations/016_create_calendar_event_groups_table.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
// Check if table calendar_event_groups already exists
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'calendar_event_groups'");
|
||||
$table_exists = $stmt->rowCount() > 0;
|
||||
|
||||
if (!$table_exists) {
|
||||
$pdo->exec("
|
||||
CREATE TABLE `calendar_event_groups` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`calendar_event_id` INT NOT NULL,
|
||||
`bni_group_id` INT NOT NULL,
|
||||
FOREIGN KEY (`calendar_event_id`) REFERENCES `calendar_events`(`id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`bni_group_id`) REFERENCES `bni_groups`(`id`) ON DELETE CASCADE,
|
||||
UNIQUE KEY `unique_event_group` (`calendar_event_id`, `bni_group_id`)
|
||||
)
|
||||
");
|
||||
echo "Table 'calendar_event_groups' created successfully.\n";
|
||||
} else {
|
||||
echo "Table 'calendar_event_groups' already exists.\n";
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
die("Migration failed: " . $e->getMessage() . "\n");
|
||||
}
|
||||
|
||||
19
db/migrations/017_add_group_id_to_people.php
Normal file
19
db/migrations/017_add_group_id_to_people.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
require_once(__DIR__ . '/../../db/config.php');
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
// Add bni_group_id to people table
|
||||
$stmt = $pdo->query("SHOW COLUMNS FROM people LIKE 'bni_group_id'");
|
||||
if ($stmt->rowCount() == 0) {
|
||||
$pdo->exec("ALTER TABLE people ADD COLUMN bni_group_id INT NULL AFTER `role`;");
|
||||
$pdo->exec("ALTER TABLE people ADD CONSTRAINT fk_people_bni_group FOREIGN KEY (bni_group_id) REFERENCES bni_groups(id) ON DELETE SET NULL;");
|
||||
}
|
||||
|
||||
echo "Migration 017 completed successfully." . PHP_EOL;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
die("Migration 017 failed: " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
24
db/migrations/018_add_details_to_people_table.php
Normal file
24
db/migrations/018_add_details_to_people_table.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$sql = <<<SQL
|
||||
ALTER TABLE people
|
||||
ADD COLUMN nip VARCHAR(255) DEFAULT NULL,
|
||||
ADD COLUMN industry VARCHAR(255) DEFAULT NULL,
|
||||
ADD COLUMN company_size VARCHAR(255) DEFAULT NULL,
|
||||
ADD COLUMN business_description TEXT DEFAULT NULL,
|
||||
ADD COLUMN company_logo_path VARCHAR(255) DEFAULT NULL,
|
||||
ADD COLUMN person_photo_path VARCHAR(255) DEFAULT NULL,
|
||||
ADD COLUMN gains_sheet_path VARCHAR(255) DEFAULT NULL,
|
||||
ADD COLUMN top_wanted_contacts_path VARCHAR(255) DEFAULT NULL,
|
||||
ADD COLUMN top_owned_contacts_path VARCHAR(255) DEFAULT NULL;
|
||||
SQL;
|
||||
$pdo->exec($sql);
|
||||
echo "Migration 018 successfully applied: Added detail columns to people table.\n";
|
||||
} catch (PDOException $e) {
|
||||
echo "Error applying migration 018: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
40
db/migrations/019_add_extra_fields_to_people.php
Normal file
40
db/migrations/019_add_extra_fields_to_people.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
$pdo = db();
|
||||
|
||||
$columns = [
|
||||
'nip' => 'VARCHAR(255) DEFAULT NULL',
|
||||
'industry' => 'VARCHAR(255) DEFAULT NULL',
|
||||
'company_size_revenue' => 'VARCHAR(255) DEFAULT NULL',
|
||||
'business_description' => 'TEXT DEFAULT NULL',
|
||||
'company_logo_path' => 'VARCHAR(255) DEFAULT NULL',
|
||||
'person_photo_path' => 'VARCHAR(255) DEFAULT NULL',
|
||||
'gains_sheet_path' => 'VARCHAR(255) DEFAULT NULL',
|
||||
'top_wanted_contacts_path' => 'VARCHAR(255) DEFAULT NULL',
|
||||
'top_owned_contacts_path' => 'VARCHAR(255) DEFAULT NULL'
|
||||
];
|
||||
|
||||
$sql_check_columns = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'people'";
|
||||
$stmt_check = $pdo->query($sql_check_columns);
|
||||
$existing_columns = $stmt_check->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
$sql_alter_parts = [];
|
||||
foreach ($columns as $column_name => $column_definition) {
|
||||
if (!in_array($column_name, $existing_columns)) {
|
||||
$sql_alter_parts[] = "ADD COLUMN `$column_name` $column_definition";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($sql_alter_parts)) {
|
||||
$sql = "ALTER TABLE people " . implode(', ', $sql_alter_parts);
|
||||
try {
|
||||
$pdo->exec($sql);
|
||||
echo "Migration 019 completed successfully: Added new fields to people table.\n";
|
||||
} catch (PDOException $e) {
|
||||
die("Migration 019 failed: " . $e->getMessage() . "\n");
|
||||
}
|
||||
} else {
|
||||
echo "Migration 019 skipped: All columns already exist.\n";
|
||||
}
|
||||
|
||||
26
db/migrations/021_rename_camelcase_columns.php
Normal file
26
db/migrations/021_rename_camelcase_columns.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
echo "Starting migration 021: Rename camelCase columns to snake_case...\n";
|
||||
|
||||
// Process instances table
|
||||
$pdo->exec("ALTER TABLE `process_instances` CHANGE `lastActivityAt` `last_activity_at` TIMESTAMP NULL;");
|
||||
|
||||
echo "Columns in 'process_instances' table renamed.\n";
|
||||
|
||||
// Process events table
|
||||
$pdo->exec("ALTER TABLE `process_events` CHANGE `processInstanceId` `process_instance_id` INT(11) UNSIGNED NOT NULL;");
|
||||
$pdo->exec("ALTER TABLE `process_events` CHANGE `createdBy` `created_by` INT(11) UNSIGNED NOT NULL;");
|
||||
$pdo->exec("ALTER TABLE `process_events` CHANGE `createdAt` `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP;");
|
||||
|
||||
echo "Columns in 'process_events' table renamed.\n";
|
||||
|
||||
echo "Migration 021 completed successfully.\n";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
die("Migration 021 failed: " . $e->getMessage());
|
||||
}
|
||||
|
||||
22
db/migrations/022_add_is_active_to_process_definitions.php
Normal file
22
db/migrations/022_add_is_active_to_process_definitions.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
function migrate_022($pdo) {
|
||||
try {
|
||||
$sql = "ALTER TABLE process_definitions ADD COLUMN is_active TINYINT(1) NOT NULL DEFAULT 1 AFTER definition_json";
|
||||
$pdo->exec($sql);
|
||||
echo "Migration successful: is_active column added to process_definitions table.\n";
|
||||
} catch (PDOException $e) {
|
||||
// Ignore if column already exists
|
||||
if ($e->getCode() !== '42S21') {
|
||||
die("Migration failed: " . $e->getMessage() . "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If called directly, run the migration
|
||||
if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) {
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
$pdo = db();
|
||||
migrate_022($pdo);
|
||||
}
|
||||
22
db/migrations/023_add_new_member_process.php
Normal file
22
db/migrations/023_add_new_member_process.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
function migrate_023($pdo) {
|
||||
$json_definition = '{ "nodes": { "start": { "id": "start", "name": "Start", "ui_hints": { "status": "in_progress", "reason": "Oczekuje na wystawienie pro formy", "next_step": "Wystaw pro formę" } }, "wait_for_payment": { "id": "wait_for_payment", "name": "Oczekiwanie na płatność", "ui_hints": { "status": "in_progress", "reason": "Oczekuje na potwierdzenie wpływu środków", "next_step": "Potwierdź wpływ środków" } }, "notify_committee": { "id": "notify_committee", "name": "Powiadomienie komitetu", "ui_hints": { "status": "in_progress", "reason": "Oczekuje na powiadomienie komitetu", "next_step": "Powiadom komitet i przejdź do decyzji" } }, "decision_committee": { "id": "decision_committee", "name": "Decyzja komitetu", "ui_hints": { "status": "in_progress", "reason": "Oczekuje na decyzję komitetu", "next_step": "Zatwierdź lub odrzuć" } }, "end_accepted": { "id": "end_accepted", "name": "Zatwierdzony", "ui_hints": { "status": "positive", "reason": "Członek przyjęty", "next_step": "" } }, "end_rejected": { "id": "end_rejected", "name": "Odrzucony", "ui_hints": { "status": "negative", "reason": "Członek odrzucony", "next_step": "" } } }, "transitions": [ { "id": "issue_proforma", "name": "Wystaw pro formę", "from": "start", "to": "wait_for_payment", "actions": [ { "type": "set_data", "params": { "keys": ["issue_date", "document_number"] } } ] }, { "id": "confirm_payment", "name": "Potwierdź wpływ środków", "from": "wait_for_payment", "to": "notify_committee", "actions": [ { "type": "set_data", "params": { "keys": ["payment_date"] } } ] } , { "id": "proceed_to_decision", "name": "Przekaż do decyzji komitetu", "from": "notify_committee", "to": "decision_committee" }, { "id": "accept_member", "name": "Zatwierdź", "from": "decision_committee", "to": "end_accepted", "actions": [ { "type": "start_process", "params": { "process_code": "wprowadzenie-nowego-cz-onka" } } ] }, { "id": "reject_member", "name": "Odrzuć", "from": "decision_committee", "to": "end_rejected" } ], "start_node_id": "start", "eligibility_rules": [ { "type": "deny_manual_start" } ] }';
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO process_definitions (name, code, definition_json, is_active) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([
|
||||
'Obsługa przyjęcia nowego członka',
|
||||
'obsluga-przyjecia-nowego-czlonka',
|
||||
$json_definition,
|
||||
1
|
||||
]);
|
||||
}
|
||||
|
||||
// If called directly, run the migration
|
||||
if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) {
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
$pdo = db();
|
||||
migrate_023($pdo);
|
||||
echo "Migration 023 executed successfully.\n";
|
||||
}
|
||||
154
db/migrations/024_update_follow_up_process.php
Normal file
154
db/migrations/024_update_follow_up_process.php
Normal file
@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
function migrate_024()
|
||||
{
|
||||
$pdo = db();
|
||||
|
||||
$process_code = 'guest_handling';
|
||||
|
||||
$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" },
|
||||
{ "name": "note", "label": "Notes", "type": "textarea" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"transitions": [
|
||||
{
|
||||
"id": "log_wants_to_join",
|
||||
"from": "awaiting_call",
|
||||
"to": "end_positive",
|
||||
"name": "Wants to Join",
|
||||
"actions": [
|
||||
{
|
||||
"type": "set_data",
|
||||
"params": { "keys": ["call_date", "note", "outcome_status"] }
|
||||
},
|
||||
{
|
||||
"type": "start_process",
|
||||
"process_code": "obsluga-przyjecia-nowego-czlonka"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "log_declined",
|
||||
"from": "awaiting_call",
|
||||
"to": "end_negative_declined",
|
||||
"name": "Declined",
|
||||
"actions": [
|
||||
{
|
||||
"type": "set_data",
|
||||
"params": { "keys": ["call_date", "note", "outcome_status"] }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "log_no_answer",
|
||||
"from": "awaiting_call",
|
||||
"to": "decide_after_no_answer",
|
||||
"name": "No Answer",
|
||||
"actions": [
|
||||
{
|
||||
"type": "set_data",
|
||||
"params": { "keys": ["call_date", "note", "outcome_status"] }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "log_call_later",
|
||||
"from": "awaiting_call",
|
||||
"to": "awaiting_call",
|
||||
"name": "Call Later",
|
||||
"actions": [
|
||||
{
|
||||
"type": "set_data",
|
||||
"params": { "keys": ["call_date", "note", "outcome_status"] }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOT;
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE process_definitions SET definition_json = :json, start_node_id = 'awaiting_call' WHERE code = :code");
|
||||
$stmt->execute([
|
||||
':json' => $json_definition,
|
||||
':code' => $process_code,
|
||||
]);
|
||||
|
||||
echo "Migration 024 executed successfully: Updated 'guest_handling' process definition.\n";
|
||||
}
|
||||
|
||||
// Direct execution guard
|
||||
if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) {
|
||||
try {
|
||||
migrate_024();
|
||||
} catch (Exception $e) {
|
||||
echo "Migration 24 failed: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
161
db/migrations/025_update_follow_up_process_structured.php
Normal file
161
db/migrations/025_update_follow_up_process_structured.php
Normal file
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
function migrate_025()
|
||||
{
|
||||
$pdo = db();
|
||||
|
||||
$process_code = 'guest_handling';
|
||||
|
||||
$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;
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE process_definitions SET definition_json = :json, start_node_id = 'awaiting_call' WHERE code = :code");
|
||||
$stmt->execute([
|
||||
':json' => $json_definition,
|
||||
':code' => $process_code,
|
||||
]);
|
||||
|
||||
echo "Migration 025 executed successfully: Updated 'guest_handling' process definition with structured data and router node.";
|
||||
}
|
||||
|
||||
// Direct execution guard
|
||||
if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"]))
|
||||
{
|
||||
try {
|
||||
migrate_025();
|
||||
} catch (Exception $e) {
|
||||
echo "Migration 25 failed: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
23
db/migrations/030_create_meetings_table.php
Normal file
23
db/migrations/030_create_meetings_table.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$pdoconn = db();
|
||||
$pdoconn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS meetings (
|
||||
id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
bni_group_id INT(11) UNSIGNED NOT NULL,
|
||||
meeting_datetime DATETIME NOT NULL,
|
||||
meeting_key VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY meeting_key (meeting_key),
|
||||
FOREIGN KEY (bni_group_id) REFERENCES bni_groups(id) ON DELETE CASCADE
|
||||
)";
|
||||
|
||||
$pdoconn->exec($sql);
|
||||
echo "Table 'meetings' created successfully." . PHP_EOL;
|
||||
} catch (PDOException $e) {
|
||||
echo "Error creating table: " . $e->getMessage() . PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
27
db/migrations/031_create_meeting_attendance_table.php
Normal file
27
db/migrations/031_create_meeting_attendance_table.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$pdoconn = db();
|
||||
$pdoconn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS meeting_attendance (
|
||||
id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
meeting_id INT(11) UNSIGNED NOT NULL,
|
||||
person_id INT(11) UNSIGNED NOT NULL,
|
||||
attendance_status ENUM('present', 'absent', 'substitute', 'none') NOT NULL DEFAULT 'none',
|
||||
guest_survey ENUM('1', '2', '3'),
|
||||
updated_by INT(11) UNSIGNED,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY meeting_person (meeting_id, person_id),
|
||||
FOREIGN KEY (meeting_id) REFERENCES meetings(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (person_id) REFERENCES people(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (updated_by) REFERENCES people(id) ON DELETE SET NULL
|
||||
)";
|
||||
|
||||
$pdoconn->exec($sql);
|
||||
echo "Table 'meeting_attendance' created successfully." . PHP_EOL;
|
||||
} catch (PDOException $e) {
|
||||
echo "Error creating table: " . $e->getMessage() . PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
73
db/migrations/032_add_meeting_key_to_meetings.php
Normal file
73
db/migrations/032_add_meeting_key_to_meetings.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$db = db();
|
||||
echo "Starting migration 032.\n";
|
||||
|
||||
// Step 1: Add bni_group_id column
|
||||
try {
|
||||
echo "Step 1: Adding bni_group_id column...\n";
|
||||
$db->exec("ALTER TABLE meetings ADD COLUMN bni_group_id INT(11) UNSIGNED NULL;");
|
||||
echo "SUCCESS: Added nullable bni_group_id column.\n";
|
||||
} catch (PDOException $e) {
|
||||
if (strpos($e->getMessage(), 'Duplicate column name') !== false) {
|
||||
echo "INFO: bni_group_id column already exists.\n";
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Add meeting_datetime column
|
||||
try {
|
||||
echo "Step 2: Adding meeting_datetime column...\n";
|
||||
$db->exec("ALTER TABLE meetings ADD COLUMN meeting_datetime DATETIME NULL;");
|
||||
echo "SUCCESS: Added nullable meeting_datetime column.\n";
|
||||
} catch (PDOException $e) {
|
||||
if (strpos($e->getMessage(), 'Duplicate column name') !== false) {
|
||||
echo "INFO: meeting_datetime column already exists.\n";
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Add meeting_key column
|
||||
try {
|
||||
echo "Step 3: Adding meeting_key column...\n";
|
||||
$db->exec("ALTER TABLE meetings ADD COLUMN meeting_key VARCHAR(255) NULL;");
|
||||
echo "SUCCESS: Added nullable meeting_key column.\n";
|
||||
} catch (PDOException $e) {
|
||||
if (strpos($e->getMessage(), 'Duplicate column name') !== false) {
|
||||
echo "INFO: meeting_key column already exists.\n";
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Populate meeting_key
|
||||
echo "Step 4: Populating meeting_key...\n";
|
||||
$updateStmt = $db->prepare("UPDATE meetings SET meeting_key = CONCAT(bni_group_id, '_', meeting_datetime) WHERE (meeting_key IS NULL OR meeting_key = '') AND bni_group_id IS NOT NULL AND meeting_datetime IS NOT NULL");
|
||||
$updateStmt->execute();
|
||||
echo "SUCCESS: Populated meeting_key for " . $updateStmt->rowCount() . " rows.\n";
|
||||
|
||||
// Step 5: Add unique index on meeting_key
|
||||
try {
|
||||
echo "Step 5: Adding unique index on meeting_key...\n";
|
||||
$db->exec("ALTER TABLE meetings ADD UNIQUE (meeting_key);");
|
||||
echo "SUCCESS: Added unique index on meeting_key.\n";
|
||||
} catch (PDOException $e) {
|
||||
if (strpos($e->getMessage(), 'Duplicate entry') !== false) {
|
||||
echo "WARNING: Could not add UNIQUE index because duplicate meeting_key values exist.\n";
|
||||
} elseif (strpos($e->getMessage(), 'already exists') !== false) {
|
||||
echo "INFO: Unique index on meeting_key already exists.\n";
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
echo "Migration 032 completed successfully.\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Migration 032 failed: " . $e->getMessage());
|
||||
die("FATAL: Migration 032 failed: " . $e->getMessage() . "\n");
|
||||
}
|
||||
71
db/migrations/033_standardize_bni_group_id.php
Normal file
71
db/migrations/033_standardize_bni_group_id.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$db = db();
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
echo "Starting migration 033: Standardize bni_group_id in meeting_attendance.
|
||||
";
|
||||
|
||||
// Ensure bni_group_id column exists and is nullable for now
|
||||
$stmt_bni = $db->query("SHOW COLUMNS FROM meeting_attendance LIKE 'bni_group_id'");
|
||||
$result_bni = $stmt_bni->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$result_bni) {
|
||||
$db->exec("ALTER TABLE meeting_attendance ADD COLUMN bni_group_id INT(11) NULL;");
|
||||
echo "SUCCESS: Added nullable 'bni_group_id' column.
|
||||
";
|
||||
} else {
|
||||
$db->exec("ALTER TABLE meeting_attendance MODIFY bni_group_id INT(11) NULL;");
|
||||
echo "SUCCESS: Modified 'bni_group_id' to be nullable.
|
||||
";
|
||||
}
|
||||
|
||||
// Check for orphaned bni_group_ids
|
||||
$orphan_check = $db->query("SELECT ma.bni_group_id, COUNT(*) as count FROM meeting_attendance ma LEFT JOIN bni_groups bg ON ma.bni_group_id = bg.id WHERE bg.id IS NULL AND ma.bni_group_id IS NOT NULL GROUP BY ma.bni_group_id;");
|
||||
$orphans = $orphan_check->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (count($orphans) > 0) {
|
||||
echo "WARNING: Found orphaned bni_group_ids in meeting_attendance table.
|
||||
";
|
||||
foreach ($orphans as $orphan) {
|
||||
echo " - bni_group_id: " . $orphan['bni_group_id'] . " (" . $orphan['count'] . " rows)
|
||||
";
|
||||
}
|
||||
// For now, we will set them to NULL to allow FK creation
|
||||
foreach ($orphans as $orphan) {
|
||||
if ($orphan['bni_group_id'] !== 0) { // we don't want to update rows that have 0 as this is default
|
||||
$update_stmt = $db->prepare("UPDATE meeting_attendance SET bni_group_id = NULL WHERE bni_group_id = :orphan_id;");
|
||||
$update_stmt->execute(['orphan_id' => $orphan['bni_group_id']]);
|
||||
echo "Set " . $update_stmt->rowCount() . " rows with orphaned bni_group_id " . $orphan['bni_group_id'] . " to NULL.
|
||||
";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo "INFO: No orphaned bni_group_ids found.
|
||||
";
|
||||
}
|
||||
|
||||
// Add foreign key if it doesn't exist
|
||||
$fk_check = $db->query("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'meeting_attendance' AND COLUMN_NAME = 'bni_group_id' AND REFERENCED_TABLE_NAME = 'bni_groups';")->fetch();
|
||||
if (!$fk_check) {
|
||||
echo "Adding foreign key on 'bni_group_id' to 'bni_groups.id'.
|
||||
";
|
||||
$db->exec("ALTER TABLE meeting_attendance ADD CONSTRAINT fk_meeting_attendance_bni_group FOREIGN KEY (bni_group_id) REFERENCES bni_groups(id) ON DELETE CASCADE;");
|
||||
echo "SUCCESS: Foreign key added.
|
||||
";
|
||||
} else {
|
||||
echo "INFO: Foreign key on 'bni_group_id' already exists.
|
||||
";
|
||||
}
|
||||
|
||||
// Now, alter the column to be NOT NULL. This will fail if there are any NULLs left.
|
||||
// I will not do this for now, as the user might want to check the orphaned data first.
|
||||
// I will let the column be nullable for now.
|
||||
|
||||
echo "Migration 033 completed successfully (bni_group_id is currently nullable).
|
||||
";
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Migration 033 failed: " . $e->getMessage());
|
||||
die("FATAL: Migration 033 failed: " . $e->getMessage() . "\n");
|
||||
}
|
||||
37
db/migrations/034_set_bni_group_id_not_null.php
Normal file
37
db/migrations/034_set_bni_group_id_not_null.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
try {
|
||||
$db = db();
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
echo "Starting migration 034: Set bni_group_id in meeting_attendance to NOT NULL.\n";
|
||||
|
||||
// First, check if there are any NULL values in the bni_group_id column.
|
||||
$null_check = $db->query("SELECT COUNT(*) FROM meeting_attendance WHERE bni_group_id IS NULL;")->fetchColumn();
|
||||
|
||||
if ($null_check > 0) {
|
||||
echo "WARNING: Found $null_check rows with NULL bni_group_id. Attempting to populate them.\n";
|
||||
|
||||
// Try to populate from the corresponding meeting record
|
||||
$update_sql = "UPDATE meeting_attendance ma JOIN meetings m ON ma.meeting_id = m.id SET ma.bni_group_id = m.bni_group_id WHERE ma.bni_group_id IS NULL AND m.bni_group_id IS NOT NULL;";
|
||||
$stmt = $db->exec($update_sql);
|
||||
echo "Populated $stmt rows based on meeting ID.\n";
|
||||
|
||||
// Re-check for NULLs
|
||||
$null_check_after = $db->query("SELECT COUNT(*) FROM meeting_attendance WHERE bni_group_id IS NULL;")->fetchColumn();
|
||||
if($null_check_after > 0) {
|
||||
// If still NULLs, we have to fail.
|
||||
throw new Exception("$null_check_after rows still have NULL bni_group_id after population attempt. Cannot proceed.");
|
||||
}
|
||||
}
|
||||
|
||||
echo "No NULL values found in bni_group_id. Altering column to NOT NULL.\n";
|
||||
$db->exec("ALTER TABLE meeting_attendance MODIFY bni_group_id INT(11) NOT NULL;");
|
||||
echo "SUCCESS: Column bni_group_id in meeting_attendance is now NOT NULL.\n";
|
||||
|
||||
echo "Migration 034 completed successfully.\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Migration 034 failed: " . $e->getMessage());
|
||||
die("FATAL: Migration 034 failed: " . $e->getMessage() . "\n");
|
||||
}
|
||||
18
db/migrations/035_cleanup_meetings_table.php
Normal file
18
db/migrations/035_cleanup_meetings_table.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../db/config.php';
|
||||
|
||||
$db = db();
|
||||
|
||||
// Step 1: Back-fill bni_group_id from group_id where bni_group_id is NULL
|
||||
$db->exec("UPDATE meetings SET bni_group_id = group_id WHERE bni_group_id IS NULL");
|
||||
|
||||
// Step 2: Modify bni_group_id to be NOT NULL
|
||||
$db->exec("ALTER TABLE meetings MODIFY COLUMN bni_group_id INT(11) NOT NULL");
|
||||
|
||||
// Step 3: Find and drop the foreign key constraint on group_id
|
||||
$db->exec("ALTER TABLE meetings DROP FOREIGN KEY `meetings_ibfk_1`");
|
||||
|
||||
// Step 4: Drop the old group_id column
|
||||
$db->exec("ALTER TABLE meetings DROP COLUMN group_id");
|
||||
|
||||
echo "Migration 035 cleanup meetings table applied successfully.";
|
||||
134
db_setup.php
Normal file
134
db_setup.php
Normal file
@ -0,0 +1,134 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
echo "Starting database setup...\n";
|
||||
|
||||
// 1. People table (unified table for users and contacts)
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS `people` (
|
||||
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`first_name` VARCHAR(255) NOT NULL,
|
||||
`last_name` VARCHAR(255) NOT NULL,
|
||||
`email` VARCHAR(255) NOT NULL UNIQUE,
|
||||
`password` VARCHAR(255) NULL,
|
||||
`company_name` VARCHAR(255) DEFAULT NULL,
|
||||
`phone` VARCHAR(50) DEFAULT NULL,
|
||||
`role` ENUM('admin', 'team_member', 'member', 'guest') NOT NULL DEFAULT 'guest',
|
||||
`is_user` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
`active` BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)");
|
||||
echo "People table created or already exists.\n";
|
||||
|
||||
// Seed default admin user
|
||||
$stmt = $pdo->prepare("SELECT id FROM people WHERE email = ?");
|
||||
$stmt->execute(['admin@example.com']);
|
||||
if ($stmt->fetchColumn() === false) {
|
||||
$password = password_hash('password', PASSWORD_DEFAULT);
|
||||
$insert_stmt = $pdo->prepare(
|
||||
"INSERT INTO people (email, password, role, first_name, last_name, is_user, active) VALUES (?, ?, 'admin', 'Admin', 'User', TRUE, TRUE)"
|
||||
);
|
||||
$insert_stmt->execute(['admin@example.com', $password]);
|
||||
echo "Default admin user created. Email: admin@example.com, Password: password\n";
|
||||
}
|
||||
|
||||
// 2. Process Definitions table
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS `process_definitions` (
|
||||
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`code` VARCHAR(255) NOT NULL UNIQUE,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`description` TEXT,
|
||||
`version` INT NOT NULL DEFAULT 1,
|
||||
`is_active` BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
`start_node_id` VARCHAR(255),
|
||||
`definition_json` TEXT,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)");
|
||||
echo "Process definitions table created or already exists.\n";
|
||||
|
||||
// Seed process_definitions
|
||||
$processes = [
|
||||
['code' => 'mentoring', 'name' => 'Mentoring nowego czlonka', 'description' => 'Proces wdrozenia nowego czlonka do organizacji.'],
|
||||
['code' => 'meeting_preparation', 'name' => 'Przygotowanie spotkania grupy', 'description' => 'Proces przygotowania do spotkania grupy, w tym agenda, materialy, etc.'],
|
||||
['code' => 'guest_handling', 'name' => 'Obsluga goscia', 'description' => 'Proces obslugi gosci odwiedzajacych organizacje.']
|
||||
];
|
||||
$stmt = $pdo->prepare("SELECT id FROM process_definitions WHERE code = ?");
|
||||
$insert_stmt = $pdo->prepare("INSERT INTO process_definitions (code, name, description) VALUES (?, ?, ?)");
|
||||
foreach ($processes as $process) {
|
||||
$stmt->execute([$process['code']]);
|
||||
if ($stmt->fetchColumn() === false) {
|
||||
$insert_stmt->execute([$process['code'], $process['name'], $process['description']]);
|
||||
echo "Seeded process: " . $process['name'] . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Process Instances table (updated FK)
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS `process_instances` (
|
||||
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`person_id` INT(11) UNSIGNED NOT NULL,
|
||||
`process_definition_id` INT(11) UNSIGNED NOT NULL,
|
||||
`current_status` VARCHAR(255) NOT NULL DEFAULT 'none',
|
||||
`current_node_id` VARCHAR(255),
|
||||
`current_reason` TEXT,
|
||||
`suggested_next_step` TEXT,
|
||||
`data_json` TEXT,
|
||||
`last_activity_at` TIMESTAMP NULL,
|
||||
FOREIGN KEY (person_id) REFERENCES people(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (process_definition_id) REFERENCES process_definitions(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY `person_process` (`person_id`, `process_definition_id`)
|
||||
)");
|
||||
echo "Process instances table created or already exists.\n";
|
||||
|
||||
// 4. Process Events table (updated FK)
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS `process_events` (
|
||||
`id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`process_instance_id` INT(11) UNSIGNED NOT NULL,
|
||||
`event_type` VARCHAR(50) NOT NULL,
|
||||
`message` TEXT,
|
||||
`node_id` VARCHAR(255),
|
||||
`payload_json` TEXT,
|
||||
`created_by` INT(11) UNSIGNED NOT NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (process_instance_id) REFERENCES process_instances(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (created_by) REFERENCES people(id) ON DELETE CASCADE
|
||||
)");
|
||||
echo "Process events table created or already exists.\n";
|
||||
|
||||
// MIGRATIONS
|
||||
echo "Starting migrations...\n";
|
||||
|
||||
// Migration: Rename `processId` to `processDefinitionId` in `process_instances`
|
||||
$stmt = $pdo->query("SHOW COLUMNS FROM `process_instances` LIKE 'processId'");
|
||||
if ($stmt->fetch()) {
|
||||
$checkFk = $pdo->query("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA = SCHEMA() AND TABLE_NAME = 'process_instances' AND COLUMN_NAME = 'processId';")->fetch();
|
||||
if ($checkFk) {
|
||||
$pdo->exec("ALTER TABLE `process_instances` DROP FOREIGN KEY `{$checkFk['CONSTRAINT_NAME']}`;");
|
||||
}
|
||||
$pdo->exec("ALTER TABLE `process_instances` CHANGE `processId` `process_definition_id` INT(11) UNSIGNED NOT NULL;");
|
||||
$pdo->exec("ALTER TABLE `process_instances` ADD FOREIGN KEY (`process_definition_id`) REFERENCES `process_definitions`(`id`) ON DELETE CASCADE;");
|
||||
echo "Migrated process_instances: processId -> processDefinitionId.\n";
|
||||
}
|
||||
|
||||
// Migration: Rename `contactId` to `personId` and update foreign key in `process_instances`
|
||||
$stmt = $pdo->query("SHOW COLUMNS FROM `process_instances` LIKE 'contactId'");
|
||||
if ($stmt->fetch()) {
|
||||
$checkFk = $pdo->query("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA = SCHEMA() AND TABLE_NAME = 'process_instances' AND COLUMN_NAME = 'contactId';")->fetch();
|
||||
if ($checkFk) {
|
||||
$pdo->exec("ALTER TABLE `process_instances` DROP FOREIGN KEY `{$checkFk['CONSTRAINT_NAME']}`;");
|
||||
}
|
||||
$pdo->exec("ALTER TABLE `process_instances` CHANGE `contactId` `person_id` INT(11) UNSIGNED NOT NULL;");
|
||||
echo "Migrated process_instances: contactId -> personId.\n";
|
||||
}
|
||||
|
||||
|
||||
// Drop old tables if they exist
|
||||
$pdo->exec("DROP TABLE IF EXISTS `users`, `contacts`;");
|
||||
echo "Dropped old 'users' and 'contacts' tables.\n";
|
||||
|
||||
echo "\nDatabase setup/update completed successfully.\n";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
die("Database setup failed: " . $e->getMessage());
|
||||
}
|
||||
185
event_types.php
Normal file
185
event_types.php
Normal file
@ -0,0 +1,185 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
include '_header.php';
|
||||
include '_navbar.php';
|
||||
|
||||
$pdo = db();
|
||||
$stmt = $pdo->query("SELECT * FROM event_types ORDER BY display_order");
|
||||
$event_types = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<?php include '_sidebar.php'; ?>
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<h1 class="h2 pt-3 pb-2 mb-3 border-bottom">Event Types</h1>
|
||||
|
||||
<?php if (isset($_SESSION['success_message'])): ?>
|
||||
<div class="alert alert-success alert-dismissible fade show mt-3" role="alert">
|
||||
<?= $_SESSION['success_message']; ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php unset($_SESSION['success_message']); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
|
||||
Add New Event Type
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive mt-4">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 30px;"></th>
|
||||
<th>Name</th>
|
||||
<th>Color</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sortable-list">
|
||||
<?php foreach ($event_types as $type): ?>
|
||||
<tr data-id="<?= $type['id'] ?>">
|
||||
<td class="handle"><i class="bi bi-grip-vertical"></i></td>
|
||||
<td><?= htmlspecialchars($type['name']) ?></td>
|
||||
<td><span class="badge" style="background-color: <?= htmlspecialchars($type['color']) ?>"><?= htmlspecialchars($type['color']) ?></span></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-warning btn-sm" data-bs-toggle="modal" data-bs-target="#editModal" data-id="<?= $type['id'] ?>" data-name="<?= htmlspecialchars($type['name']) ?>" data-color="<?= htmlspecialchars($type['color']) ?>" data-display-order="<?= htmlspecialchars($type['display_order']) ?>">
|
||||
Edit
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteModal" data-id="<?= $type['id'] ?>">
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Modal -->
|
||||
<div class="modal fade" id="addModal" tabindex="-1" aria-labelledby="addModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addModalLabel">Add Event Type</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="_add_event_type.php" method="post">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="addName" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="addName" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addColor" class="form-label">Color</label>
|
||||
<input type="color" class="form-control" id="addColor" name="color" value="#007bff" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" name="add" class="btn btn-primary">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editModalLabel">Edit Event Type</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="_update_event_type.php" method="post">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="editId" name="id">
|
||||
<input type="hidden" id="editDisplayOrder" name="display_order">
|
||||
<div class="mb-3">
|
||||
<label for="editName" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="editName" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editColor" class="form-label">Color</label>
|
||||
<input type="color" class="form-control" id="editColor" name="color" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" name="edit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">Delete Event Type</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to delete this event type?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<a href="#" id="deleteLink" class="btn btn-danger">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<?php include '_footer.php'; ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var editModal = document.getElementById('editModal');
|
||||
editModal.addEventListener('show.bs.modal', function (event) {
|
||||
var button = event.relatedTarget;
|
||||
var id = button.getAttribute('data-id');
|
||||
var name = button.getAttribute('data-name');
|
||||
var color = button.getAttribute('data-color');
|
||||
var display_order = button.getAttribute('data-display-order');
|
||||
|
||||
var modalTitle = editModal.querySelector('.modal-title');
|
||||
var idInput = editModal.querySelector('#editId');
|
||||
var nameInput = editModal.querySelector('#editName');
|
||||
var colorInput = editModal.querySelector('#editColor');
|
||||
var displayOrderInput = editModal.querySelector('#editDisplayOrder');
|
||||
|
||||
idInput.value = id;
|
||||
nameInput.value = name;
|
||||
colorInput.value = color;
|
||||
displayOrderInput.value = display_order;
|
||||
});
|
||||
|
||||
var deleteModal = document.getElementById('deleteModal');
|
||||
deleteModal.addEventListener('show.bs.modal', function (event) {
|
||||
var button = event.relatedTarget;
|
||||
var id = button.getAttribute('data-id');
|
||||
var deleteLink = deleteModal.querySelector('#deleteLink');
|
||||
deleteLink.href = '_delete_event_type.php?id=' + id;
|
||||
});
|
||||
|
||||
$(function() {
|
||||
$("#sortable-list").sortable({
|
||||
handle: ".handle",
|
||||
update: function(event, ui) {
|
||||
var order = $(this).sortable('toArray', {attribute: 'data-id'});
|
||||
$.post('_update_event_type_order.php', { order: order });
|
||||
}
|
||||
}).disableSelection();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
195
functions.php
Normal file
195
functions.php
Normal file
@ -0,0 +1,195 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
include '_header.php';
|
||||
include '_navbar.php';
|
||||
|
||||
$pdo = db();
|
||||
$stmt = $pdo->query("SELECT f.*, bg.name as group_name FROM functions f LEFT JOIN bni_groups bg ON f.bni_group_id = bg.id ORDER BY f.display_order");
|
||||
$functions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$group_stmt = $pdo->query("SELECT * FROM bni_groups WHERE active = 1 ORDER BY name");
|
||||
$bni_groups = $group_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
|
||||
?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<?php include '_sidebar.php'; ?>
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<h1 class="h2 pt-3 pb-2 mb-3 border-bottom">Functions</h1>
|
||||
|
||||
<?php if (isset($_SESSION['success_message'])): ?>
|
||||
<div class="alert alert-success alert-dismissible fade show mt-3" function="alert">
|
||||
<?= $_SESSION['success_message']; ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php unset($_SESSION['success_message']); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
|
||||
Add New Function
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive mt-4">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 30px;"></th>
|
||||
<th>Name</th>
|
||||
<th>Group</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sortable-list">
|
||||
<?php foreach ($functions as $function): ?>
|
||||
<tr data-id="<?= $function['id'] ?>">
|
||||
<td class="handle"><i class="bi bi-grip-vertical"></i></td>
|
||||
<td><?= htmlspecialchars($function['name']) ?></td>
|
||||
<td><?= htmlspecialchars($function['group_name']) ?></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-warning btn-sm" data-bs-toggle="modal" data-bs-target="#editModal" data-id="<?= $function['id'] ?>" data-name="<?= htmlspecialchars($function['name']) ?>" data-bni_group_id="<?= $function['bni_group_id'] ?>">
|
||||
Edit
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteModal" data-id="<?= $function['id'] ?>">
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Modal -->
|
||||
<div class="modal fade" id="addModal" tabindex="-1" aria-labelledby="addModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addModalLabel">Add Function</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="_add_function.php" method="post">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="addName" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="addName" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addBniGroup" class="form-label">Group</label>
|
||||
<select class="form-select" id="addBniGroup" name="bni_group_id" required>
|
||||
<option value="">Select a group</option>
|
||||
<?php foreach ($bni_groups as $group): ?>
|
||||
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" name="add" class="btn btn-primary">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editModalLabel">Edit Function</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="_update_function.php" method="post">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="editId" name="id">
|
||||
<div class="mb-3">
|
||||
<label for="editName" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="editName" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editBniGroup" class="form-label">Group</label>
|
||||
<select class="form-select" id="editBniGroup" name="bni_group_id" required>
|
||||
<option value="">Select a group</option>
|
||||
<?php foreach ($bni_groups as $group): ?>
|
||||
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" name="edit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">Delete Function</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to delete this function?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<a href="#" id="deleteLink" class="btn btn-danger">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<?php include '_footer.php'; ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var editModal = document.getElementById('editModal');
|
||||
editModal.addEventListener('show.bs.modal', function (event) {
|
||||
var button = event.relatedTarget;
|
||||
var id = button.getAttribute('data-id');
|
||||
var name = button.getAttribute('data-name');
|
||||
var bni_group_id = button.getAttribute('data-bni_group_id');
|
||||
|
||||
var modalTitle = editModal.querySelector('.modal-title');
|
||||
var idInput = editModal.querySelector('#editId');
|
||||
var nameInput = editModal.querySelector('#editName');
|
||||
var groupSelect = editModal.querySelector('#editBniGroup');
|
||||
|
||||
idInput.value = id;
|
||||
nameInput.value = name;
|
||||
groupSelect.value = bni_group_id;
|
||||
});
|
||||
|
||||
var deleteModal = document.getElementById('deleteModal');
|
||||
deleteModal.addEventListener('show.bs.modal', function (event) {
|
||||
var button = event.relatedTarget;
|
||||
var id = button.getAttribute('data-id');
|
||||
var deleteLink = deleteModal.querySelector('#deleteLink');
|
||||
deleteLink.href = '_delete_function.php?id=' + id;
|
||||
});
|
||||
|
||||
$(function() {
|
||||
$("#sortable-list").sortable({
|
||||
handle: ".handle",
|
||||
update: function(event, ui) {
|
||||
var order = $(this).sortable('toArray', {attribute: 'data-id'});
|
||||
$.post('_update_function_order.php', { order: order });
|
||||
}
|
||||
}).disableSelection();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
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());
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user