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, code, name, definition_json, is_active FROM process_definitions WHERE is_active = 1 AND is_latest = 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'], 'code' => $def['code'], '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 { $inTransaction = $this->pdo->inTransaction(); if (!$inTransaction) { $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.");