"" GROUP BY a.cycle_key ORDER BY COALESCE(MAX(a.cycle_start_at), MAX(a.assigned_at)) DESC, a.cycle_key DESC LIMIT ' . $limit, 'i', [$taskId] ); } public function findCycleKeyForTask(int $taskId, string $cycleKey): ?array { $cycleKey = trim($cycleKey); if ($taskId <= 0 || $cycleKey === '') { return null; } return TaskCertDb::fetchOne( 'SELECT a.cycle_key, MIN(a.cycle_start_at) AS cycle_start_at, MAX(a.cycle_end_at) AS cycle_end_at, COUNT(*) AS assignment_count FROM task_assignment a WHERE a.task_id = ? AND a.cycle_key = ? GROUP BY a.cycle_key LIMIT 1', 'is', [$taskId, $cycleKey] ); } public function getKpisForTaskLatest(int $taskId): array { $statusRows = TaskCertDb::fetchAll( 'SELECT a.status, COUNT(*) AS cnt FROM task_assignment a INNER JOIN ( SELECT main_contact_id, MAX(id) AS latest_id FROM task_assignment WHERE task_id = ? GROUP BY main_contact_id ) latest ON latest.latest_id = a.id GROUP BY a.status', 'i', [$taskId] ); $statusCounts = [ 'open' => 0, 'in_progress' => 0, 'approval_pending' => 0, 'overdue' => 0, 'completed' => 0, 'expired' => 0, 'rejected' => 0, ]; foreach ($statusRows as $row) { $status = (string) ($row['status'] ?? ''); if (!array_key_exists($status, $statusCounts)) { continue; } $statusCounts[$status] = (int) ($row['cnt'] ?? 0); } $totalAssigned = array_sum($statusCounts); $completedCount = (int) $statusCounts['completed']; $todoCount = $totalAssigned - $completedCount; $awaitingApprovalCount = (int) TaskCertDb::scalar( 'SELECT COUNT(*) FROM task_submission s INNER JOIN ( SELECT main_contact_id, MAX(id) AS latest_id FROM task_assignment WHERE task_id = ? GROUP BY main_contact_id ) latest ON latest.latest_id = s.assignment_id WHERE s.status = "awaiting_approval"', 'i', [$taskId] ); $escalatedCount = (int) TaskCertDb::scalar( 'SELECT COUNT(*) FROM task_assignment a INNER JOIN ( SELECT main_contact_id, MAX(id) AS latest_id FROM task_assignment WHERE task_id = ? GROUP BY main_contact_id ) latest ON latest.latest_id = a.id WHERE a.escalation_level > 0', 'i', [$taskId] ); $withSubmissionCount = (int) TaskCertDb::scalar( 'SELECT COUNT(DISTINCT s.assignment_id) FROM task_submission s INNER JOIN ( SELECT main_contact_id, MAX(id) AS latest_id FROM task_assignment WHERE task_id = ? GROUP BY main_contact_id ) latest ON latest.latest_id = s.assignment_id', 'i', [$taskId] ); $completionRate = $totalAssigned > 0 ? round(($completedCount / $totalAssigned) * 100, 1) : 0.0; return [ 'total_assigned' => $totalAssigned, 'completed_count' => $completedCount, 'todo_count' => $todoCount, 'overdue_count' => (int) $statusCounts['overdue'], 'awaiting_approval_count' => $awaitingApprovalCount, 'escalated_count' => $escalatedCount, 'with_submission_count' => $withSubmissionCount, 'completion_rate' => $completionRate, 'status_counts' => $statusCounts, ]; } public function listTodoAssignmentsLatest(int $taskId, array $filters = [], int $limit = 100): array { $limit = max(1, min(500, $limit)); $where = [ 'a.status <> "completed"', ]; $types = 'i'; $params = [$taskId]; $status = (string) ($filters['status'] ?? ''); if ($status !== '') { $where[] = 'a.status = ?'; $types .= 's'; $params[] = $status; } $query = trim((string) ($filters['q'] ?? '')); if ($query !== '') { $like = '%' . $query . '%'; $where[] = '(c.name LIKE ? OR c.email LIKE ?)'; $types .= 'ss'; $params[] = $like; $params[] = $like; } $sql = 'SELECT a.*, c.name AS contact_name, c.email AS contact_email, ls.last_submitted_at FROM task_assignment a INNER JOIN ( SELECT main_contact_id, MAX(id) AS latest_id FROM task_assignment WHERE task_id = ? GROUP BY main_contact_id ) latest ON latest.latest_id = a.id LEFT JOIN main_contact c ON c.id = a.main_contact_id LEFT JOIN ( SELECT assignment_id, MAX(submitted_at) AS last_submitted_at FROM task_submission GROUP BY assignment_id ) ls ON ls.assignment_id = a.id WHERE ' . implode(' AND ', $where) . ' ORDER BY CASE a.status WHEN "overdue" THEN 1 WHEN "approval_pending" THEN 2 WHEN "open" THEN 3 WHEN "in_progress" THEN 4 WHEN "rejected" THEN 5 WHEN "expired" THEN 6 ELSE 7 END, a.due_at IS NULL, a.due_at ASC, a.id DESC LIMIT ' . $limit; return TaskCertDb::fetchAll($sql, $types, $params); } public function listCompletedAssignmentsLatest(int $taskId, array $filters = [], int $limit = 100): array { $limit = max(1, min(500, $limit)); $status = (string) ($filters['status'] ?? ''); if ($status !== '' && $status !== 'completed') { return []; } $where = [ 'a.status = "completed"', ]; $types = 'i'; $params = [$taskId]; $query = trim((string) ($filters['q'] ?? '')); if ($query !== '') { $like = '%' . $query . '%'; $where[] = '(c.name LIKE ? OR c.email LIKE ?)'; $types .= 'ss'; $params[] = $like; $params[] = $like; } $sql = 'SELECT a.*, c.name AS contact_name, c.email AS contact_email, tc.id AS certificate_id, tc.status AS certificate_status, tc.valid_from AS certificate_valid_from, tc.valid_until AS certificate_valid_until, tc.certificate_code FROM task_assignment a INNER JOIN ( SELECT main_contact_id, MAX(id) AS latest_id FROM task_assignment WHERE task_id = ? GROUP BY main_contact_id ) latest ON latest.latest_id = a.id LEFT JOIN main_contact c ON c.id = a.main_contact_id LEFT JOIN task_certificate tc ON tc.assignment_id = a.id WHERE ' . implode(' AND ', $where) . ' ORDER BY a.completed_at DESC, a.id DESC LIMIT ' . $limit; return TaskCertDb::fetchAll($sql, $types, $params); } public function listApprovalAndOverdueLatest(int $taskId, array $filters = [], int $limit = 100): array { $limit = max(1, min(500, $limit)); $query = trim((string) ($filters['q'] ?? '')); $methodType = trim((string) ($filters['method_type'] ?? '')); $submissionStatus = trim((string) ($filters['submission_status'] ?? '')); $status = trim((string) ($filters['status'] ?? '')); $approvalWhere = [ 's.status = "awaiting_approval"', ]; $approvalTypes = 'i'; $approvalParams = [$taskId]; if ($methodType !== '') { $approvalWhere[] = 'm.method_type = ?'; $approvalTypes .= 's'; $approvalParams[] = $methodType; } if ($submissionStatus !== '') { $approvalWhere[] = 's.status = ?'; $approvalTypes .= 's'; $approvalParams[] = $submissionStatus; } if ($status !== '') { $approvalWhere[] = 'a.status = ?'; $approvalTypes .= 's'; $approvalParams[] = $status; } if ($query !== '') { $like = '%' . $query . '%'; $approvalWhere[] = '(c.name LIKE ? OR c.email LIKE ?)'; $approvalTypes .= 'ss'; $approvalParams[] = $like; $approvalParams[] = $like; } $approvalSql = 'SELECT s.*, a.status AS assignment_status, a.escalation_level, a.due_at, a.overdue_at, a.cycle_key, c.name AS contact_name, c.email AS contact_email, m.method_type FROM task_submission s INNER JOIN task_assignment a ON a.id = s.assignment_id INNER JOIN ( SELECT main_contact_id, MAX(id) AS latest_id FROM task_assignment WHERE task_id = ? GROUP BY main_contact_id ) latest ON latest.latest_id = a.id INNER JOIN task_method m ON m.id = s.method_id LEFT JOIN main_contact c ON c.id = a.main_contact_id WHERE ' . implode(' AND ', $approvalWhere) . ' ORDER BY s.submitted_at ASC, s.id ASC LIMIT ' . $limit; $overdueWhere = [ 'a.status <> "completed"', '(a.status = "overdue" OR a.escalation_level > 0)', ]; $overdueTypes = 'i'; $overdueParams = [$taskId]; if ($status !== '') { $overdueWhere[] = 'a.status = ?'; $overdueTypes .= 's'; $overdueParams[] = $status; } if ($query !== '') { $like = '%' . $query . '%'; $overdueWhere[] = '(c.name LIKE ? OR c.email LIKE ?)'; $overdueTypes .= 'ss'; $overdueParams[] = $like; $overdueParams[] = $like; } $overdueSql = 'SELECT a.*, c.name AS contact_name, c.email AS contact_email, ls.last_submitted_at FROM task_assignment a INNER JOIN ( SELECT main_contact_id, MAX(id) AS latest_id FROM task_assignment WHERE task_id = ? GROUP BY main_contact_id ) latest ON latest.latest_id = a.id LEFT JOIN main_contact c ON c.id = a.main_contact_id LEFT JOIN ( SELECT assignment_id, MAX(submitted_at) AS last_submitted_at FROM task_submission GROUP BY assignment_id ) ls ON ls.assignment_id = a.id WHERE ' . implode(' AND ', $overdueWhere) . ' ORDER BY a.escalation_level DESC, a.due_at ASC, a.id DESC LIMIT ' . $limit; return [ 'approvals' => TaskCertDb::fetchAll($approvalSql, $approvalTypes, $approvalParams), 'overdue' => TaskCertDb::fetchAll($overdueSql, $overdueTypes, $overdueParams), ]; } public function listSubmissionAuditLatest(int $taskId, array $filters = [], int $limit = 100): array { $limit = max(1, min(500, $limit)); $where = []; $types = 'i'; $params = [$taskId]; $query = trim((string) ($filters['q'] ?? '')); if ($query !== '') { $like = '%' . $query . '%'; $where[] = '(c.name LIKE ? OR c.email LIKE ?)'; $types .= 'ss'; $params[] = $like; $params[] = $like; } $methodType = trim((string) ($filters['method_type'] ?? '')); if ($methodType !== '') { $where[] = 'm.method_type = ?'; $types .= 's'; $params[] = $methodType; } $submissionStatus = trim((string) ($filters['submission_status'] ?? '')); if ($submissionStatus !== '') { $where[] = 's.status = ?'; $types .= 's'; $params[] = $submissionStatus; } $status = trim((string) ($filters['status'] ?? '')); if ($status !== '') { $where[] = 'a.status = ?'; $types .= 's'; $params[] = $status; } $whereClause = $where === [] ? '' : (' AND ' . implode(' AND ', $where)); $sql = 'SELECT s.*, a.status AS assignment_status, a.cycle_key, a.due_at, a.overdue_at, a.completed_at, a.escalation_level, c.name AS contact_name, c.email AS contact_email, m.id AS method_id, m.method_type FROM task_submission s INNER JOIN task_assignment a ON a.id = s.assignment_id INNER JOIN ( SELECT main_contact_id, MAX(id) AS latest_id FROM task_assignment WHERE task_id = ? GROUP BY main_contact_id ) latest ON latest.latest_id = a.id INNER JOIN task_method m ON m.id = s.method_id LEFT JOIN main_contact c ON c.id = a.main_contact_id WHERE 1=1' . $whereClause . ' ORDER BY s.submitted_at DESC, s.id DESC LIMIT ' . $limit; return TaskCertDb::fetchAll($sql, $types, $params); } public function listSubmissionAuditLatestForExport(int $taskId): array { return TaskCertDb::fetchAll( 'SELECT s.*, a.status AS assignment_status, a.cycle_key, a.due_at, a.overdue_at, a.completed_at, a.escalation_level, c.name AS contact_name, c.email AS contact_email, m.id AS method_id, m.method_type FROM task_submission s INNER JOIN task_assignment a ON a.id = s.assignment_id INNER JOIN ( SELECT main_contact_id, MAX(id) AS latest_id FROM task_assignment WHERE task_id = ? GROUP BY main_contact_id ) latest ON latest.latest_id = a.id INNER JOIN task_method m ON m.id = s.method_id LEFT JOIN main_contact c ON c.id = a.main_contact_id ORDER BY s.submitted_at DESC, s.id DESC', 'i', [$taskId] ); } public function listMethodCompletionStatsLatest(int $taskId, array $filters = [], int $limit = 100): array { $limit = max(1, min(200, $limit)); $where = [ 'm.task_id = ?', 'm.active = 1', ]; $whereTypes = 'i'; $whereParams = [$taskId]; $methodType = trim((string) ($filters['method_type'] ?? '')); if ($methodType !== '') { $where[] = 'm.method_type = ?'; $whereTypes .= 's'; $whereParams[] = $methodType; } $joinTypes = ''; $joinParams = []; $query = trim((string) ($filters['q'] ?? '')); $assignmentContactFilter = ''; if ($query !== '') { $like = '%' . $query . '%'; $assignmentContactFilter = ' AND EXISTS ( SELECT 1 FROM main_contact c WHERE c.id = a.main_contact_id AND (c.name LIKE ? OR c.email LIKE ?) )'; $joinTypes .= 'ss'; $joinParams[] = $like; $joinParams[] = $like; } $sql = 'SELECT m.id AS method_id, m.method_type, m.is_required, m.sort_order, COUNT(DISTINCT a.id) AS assignment_count, SUM( CASE WHEN a.id IS NULL THEN 0 WHEN EXISTS ( SELECT 1 FROM task_submission s2 WHERE s2.assignment_id = a.id AND s2.method_id = m.id AND s2.status IN ("auto_passed", "approved") ) THEN 1 ELSE 0 END ) AS fulfilled_count FROM task_method m LEFT JOIN ( SELECT a1.* FROM task_assignment a1 INNER JOIN ( SELECT main_contact_id, MAX(id) AS latest_id FROM task_assignment WHERE task_id = ? GROUP BY main_contact_id ) latest ON latest.latest_id = a1.id ) a ON a.task_id = m.task_id' . $assignmentContactFilter . ' WHERE ' . implode(' AND ', $where) . ' GROUP BY m.id, m.method_type, m.is_required, m.sort_order ORDER BY m.sort_order ASC, m.id ASC LIMIT ' . $limit; // Placeholder order: derived latest task_id, JOIN filters, WHERE filters. $types = 'i' . $joinTypes . $whereTypes; $params = array_merge([$taskId], $joinParams, $whereParams); return TaskCertDb::fetchAll($sql, $types, $params); } public function getKpisForTaskCycle(int $taskId, string $cycleKey): array { $statusRows = TaskCertDb::fetchAll( 'SELECT a.status, COUNT(*) AS cnt FROM task_assignment a WHERE a.task_id = ? AND a.cycle_key = ? GROUP BY a.status', 'is', [$taskId, $cycleKey] ); $statusCounts = [ 'open' => 0, 'in_progress' => 0, 'approval_pending' => 0, 'overdue' => 0, 'completed' => 0, 'expired' => 0, 'rejected' => 0, ]; foreach ($statusRows as $row) { $status = (string) ($row['status'] ?? ''); if (!array_key_exists($status, $statusCounts)) { continue; } $statusCounts[$status] = (int) ($row['cnt'] ?? 0); } $totalAssigned = array_sum($statusCounts); $completedCount = (int) $statusCounts['completed']; $todoCount = $totalAssigned - $completedCount; $awaitingApprovalCount = (int) TaskCertDb::scalar( 'SELECT COUNT(*) FROM task_submission s INNER JOIN task_assignment a ON a.id = s.assignment_id WHERE a.task_id = ? AND a.cycle_key = ? AND s.status = "awaiting_approval"', 'is', [$taskId, $cycleKey] ); $escalatedCount = (int) TaskCertDb::scalar( 'SELECT COUNT(*) FROM task_assignment a WHERE a.task_id = ? AND a.cycle_key = ? AND a.escalation_level > 0', 'is', [$taskId, $cycleKey] ); $withSubmissionCount = (int) TaskCertDb::scalar( 'SELECT COUNT(DISTINCT s.assignment_id) FROM task_submission s INNER JOIN task_assignment a ON a.id = s.assignment_id WHERE a.task_id = ? AND a.cycle_key = ?', 'is', [$taskId, $cycleKey] ); $completionRate = $totalAssigned > 0 ? round(($completedCount / $totalAssigned) * 100, 1) : 0.0; return [ 'total_assigned' => $totalAssigned, 'completed_count' => $completedCount, 'todo_count' => $todoCount, 'overdue_count' => (int) $statusCounts['overdue'], 'awaiting_approval_count' => $awaitingApprovalCount, 'escalated_count' => $escalatedCount, 'with_submission_count' => $withSubmissionCount, 'completion_rate' => $completionRate, 'status_counts' => $statusCounts, ]; } public function listTodoAssignments(int $taskId, string $cycleKey, array $filters = [], int $limit = 100): array { $limit = max(1, min(500, $limit)); $where = [ 'a.task_id = ?', 'a.cycle_key = ?', 'a.status <> "completed"', ]; $types = 'is'; $params = [$taskId, $cycleKey]; $status = (string) ($filters['status'] ?? ''); if ($status !== '') { $where[] = 'a.status = ?'; $types .= 's'; $params[] = $status; } $query = trim((string) ($filters['q'] ?? '')); if ($query !== '') { $like = '%' . $query . '%'; $where[] = '(c.name LIKE ? OR c.email LIKE ?)'; $types .= 'ss'; $params[] = $like; $params[] = $like; } $sql = 'SELECT a.*, c.name AS contact_name, c.email AS contact_email, ls.last_submitted_at FROM task_assignment a LEFT JOIN main_contact c ON c.id = a.main_contact_id LEFT JOIN ( SELECT assignment_id, MAX(submitted_at) AS last_submitted_at FROM task_submission GROUP BY assignment_id ) ls ON ls.assignment_id = a.id WHERE ' . implode(' AND ', $where) . ' ORDER BY CASE a.status WHEN "overdue" THEN 1 WHEN "approval_pending" THEN 2 WHEN "open" THEN 3 WHEN "in_progress" THEN 4 WHEN "rejected" THEN 5 WHEN "expired" THEN 6 ELSE 7 END, a.due_at IS NULL, a.due_at ASC, a.id DESC LIMIT ' . $limit; return TaskCertDb::fetchAll($sql, $types, $params); } public function listCompletedAssignments(int $taskId, string $cycleKey, array $filters = [], int $limit = 100): array { $limit = max(1, min(500, $limit)); $status = (string) ($filters['status'] ?? ''); if ($status !== '' && $status !== 'completed') { return []; } $where = [ 'a.task_id = ?', 'a.cycle_key = ?', 'a.status = "completed"', ]; $types = 'is'; $params = [$taskId, $cycleKey]; $query = trim((string) ($filters['q'] ?? '')); if ($query !== '') { $like = '%' . $query . '%'; $where[] = '(c.name LIKE ? OR c.email LIKE ?)'; $types .= 'ss'; $params[] = $like; $params[] = $like; } $sql = 'SELECT a.*, c.name AS contact_name, c.email AS contact_email, tc.id AS certificate_id, tc.status AS certificate_status, tc.valid_from AS certificate_valid_from, tc.valid_until AS certificate_valid_until, tc.certificate_code FROM task_assignment a LEFT JOIN main_contact c ON c.id = a.main_contact_id LEFT JOIN task_certificate tc ON tc.assignment_id = a.id WHERE ' . implode(' AND ', $where) . ' ORDER BY a.completed_at DESC, a.id DESC LIMIT ' . $limit; return TaskCertDb::fetchAll($sql, $types, $params); } public function listApprovalAndOverdue(int $taskId, string $cycleKey, array $filters = [], int $limit = 100): array { $limit = max(1, min(500, $limit)); $query = trim((string) ($filters['q'] ?? '')); $methodType = trim((string) ($filters['method_type'] ?? '')); $submissionStatus = trim((string) ($filters['submission_status'] ?? '')); $status = trim((string) ($filters['status'] ?? '')); $approvalWhere = [ 'a.task_id = ?', 'a.cycle_key = ?', 's.status = "awaiting_approval"', ]; $approvalTypes = 'is'; $approvalParams = [$taskId, $cycleKey]; if ($methodType !== '') { $approvalWhere[] = 'm.method_type = ?'; $approvalTypes .= 's'; $approvalParams[] = $methodType; } if ($submissionStatus !== '') { $approvalWhere[] = 's.status = ?'; $approvalTypes .= 's'; $approvalParams[] = $submissionStatus; } if ($status !== '') { $approvalWhere[] = 'a.status = ?'; $approvalTypes .= 's'; $approvalParams[] = $status; } if ($query !== '') { $like = '%' . $query . '%'; $approvalWhere[] = '(c.name LIKE ? OR c.email LIKE ?)'; $approvalTypes .= 'ss'; $approvalParams[] = $like; $approvalParams[] = $like; } $approvalSql = 'SELECT s.*, a.status AS assignment_status, a.escalation_level, a.due_at, a.overdue_at, a.cycle_key, c.name AS contact_name, c.email AS contact_email, m.method_type FROM task_submission s INNER JOIN task_assignment a ON a.id = s.assignment_id INNER JOIN task_method m ON m.id = s.method_id LEFT JOIN main_contact c ON c.id = a.main_contact_id WHERE ' . implode(' AND ', $approvalWhere) . ' ORDER BY s.submitted_at ASC, s.id ASC LIMIT ' . $limit; $overdueWhere = [ 'a.task_id = ?', 'a.cycle_key = ?', 'a.status <> "completed"', '(a.status = "overdue" OR a.escalation_level > 0)', ]; $overdueTypes = 'is'; $overdueParams = [$taskId, $cycleKey]; if ($status !== '') { $overdueWhere[] = 'a.status = ?'; $overdueTypes .= 's'; $overdueParams[] = $status; } if ($query !== '') { $like = '%' . $query . '%'; $overdueWhere[] = '(c.name LIKE ? OR c.email LIKE ?)'; $overdueTypes .= 'ss'; $overdueParams[] = $like; $overdueParams[] = $like; } $overdueSql = 'SELECT a.*, c.name AS contact_name, c.email AS contact_email, ls.last_submitted_at FROM task_assignment a LEFT JOIN main_contact c ON c.id = a.main_contact_id LEFT JOIN ( SELECT assignment_id, MAX(submitted_at) AS last_submitted_at FROM task_submission GROUP BY assignment_id ) ls ON ls.assignment_id = a.id WHERE ' . implode(' AND ', $overdueWhere) . ' ORDER BY a.escalation_level DESC, a.due_at ASC, a.id DESC LIMIT ' . $limit; return [ 'approvals' => TaskCertDb::fetchAll($approvalSql, $approvalTypes, $approvalParams), 'overdue' => TaskCertDb::fetchAll($overdueSql, $overdueTypes, $overdueParams), ]; } public function listSubmissionAudit(int $taskId, string $cycleKey, array $filters = [], int $limit = 100): array { $limit = max(1, min(500, $limit)); $where = [ 'a.task_id = ?', 'a.cycle_key = ?', ]; $types = 'is'; $params = [$taskId, $cycleKey]; $query = trim((string) ($filters['q'] ?? '')); if ($query !== '') { $like = '%' . $query . '%'; $where[] = '(c.name LIKE ? OR c.email LIKE ?)'; $types .= 'ss'; $params[] = $like; $params[] = $like; } $methodType = trim((string) ($filters['method_type'] ?? '')); if ($methodType !== '') { $where[] = 'm.method_type = ?'; $types .= 's'; $params[] = $methodType; } $submissionStatus = trim((string) ($filters['submission_status'] ?? '')); if ($submissionStatus !== '') { $where[] = 's.status = ?'; $types .= 's'; $params[] = $submissionStatus; } $status = trim((string) ($filters['status'] ?? '')); if ($status !== '') { $where[] = 'a.status = ?'; $types .= 's'; $params[] = $status; } $sql = 'SELECT s.*, a.status AS assignment_status, a.cycle_key, a.due_at, a.overdue_at, a.completed_at, a.escalation_level, c.name AS contact_name, c.email AS contact_email, m.id AS method_id, m.method_type FROM task_submission s INNER JOIN task_assignment a ON a.id = s.assignment_id INNER JOIN task_method m ON m.id = s.method_id LEFT JOIN main_contact c ON c.id = a.main_contact_id WHERE ' . implode(' AND ', $where) . ' ORDER BY s.submitted_at DESC, s.id DESC LIMIT ' . $limit; return TaskCertDb::fetchAll($sql, $types, $params); } public function listSubmissionAuditForCycleExport(int $taskId, string $cycleKey): array { return TaskCertDb::fetchAll( 'SELECT s.*, a.status AS assignment_status, a.cycle_key, a.due_at, a.overdue_at, a.completed_at, a.escalation_level, c.name AS contact_name, c.email AS contact_email, m.id AS method_id, m.method_type FROM task_submission s INNER JOIN task_assignment a ON a.id = s.assignment_id INNER JOIN task_method m ON m.id = s.method_id LEFT JOIN main_contact c ON c.id = a.main_contact_id WHERE a.task_id = ? AND a.cycle_key = ? ORDER BY s.submitted_at DESC, s.id DESC', 'is', [$taskId, $cycleKey] ); } public function listMethodCompletionStats(int $taskId, string $cycleKey, array $filters = [], int $limit = 100): array { $limit = max(1, min(200, $limit)); $where = [ 'm.task_id = ?', 'm.active = 1', ]; $whereTypes = 'i'; $whereParams = [$taskId]; $methodType = trim((string) ($filters['method_type'] ?? '')); if ($methodType !== '') { $where[] = 'm.method_type = ?'; $whereTypes .= 's'; $whereParams[] = $methodType; } $joinTypes = ''; $joinParams = []; $query = trim((string) ($filters['q'] ?? '')); $assignmentContactFilter = ''; if ($query !== '') { $like = '%' . $query . '%'; $assignmentContactFilter = ' AND EXISTS ( SELECT 1 FROM main_contact c WHERE c.id = a.main_contact_id AND (c.name LIKE ? OR c.email LIKE ?) )'; $joinTypes .= 'ss'; $joinParams[] = $like; $joinParams[] = $like; } $sql = 'SELECT m.id AS method_id, m.method_type, m.is_required, m.sort_order, COUNT(DISTINCT a.id) AS assignment_count, SUM( CASE WHEN a.id IS NULL THEN 0 WHEN EXISTS ( SELECT 1 FROM task_submission s2 WHERE s2.assignment_id = a.id AND s2.method_id = m.id AND s2.status IN ("auto_passed", "approved") ) THEN 1 ELSE 0 END ) AS fulfilled_count FROM task_method m LEFT JOIN task_assignment a ON a.task_id = m.task_id AND a.cycle_key = ?' . $assignmentContactFilter . ' WHERE ' . implode(' AND ', $where) . ' GROUP BY m.id, m.method_type, m.is_required, m.sort_order ORDER BY m.sort_order ASC, m.id ASC LIMIT ' . $limit; // Placeholder order: cycle key, JOIN filters, WHERE filters. $types = 's' . $joinTypes . $whereTypes; $params = array_merge([$cycleKey], $joinParams, $whereParams); return TaskCertDb::fetchAll($sql, $types, $params); } }