*/ function qdb_analytics_submissions_by_day(PDO $pdo, array $tokenRec, int $days = 14): array { $days = max(1, min(90, $days)); [$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl'); $startTs = strtotime('today', time()) - ($days - 1) * 86400; $stmt = $pdo->prepare( "SELECT qs.submittedAt FROM questionnaire_submission qs INNER JOIN client cl ON cl.clientCode = qs.clientCode WHERE $rbacClause AND qs.submittedAt >= :start" ); $stmt->execute(array_merge($rbacParams, [':start' => $startTs])); $byDate = []; foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $submittedAt) { $key = date('Y-m-d', (int)$submittedAt); $byDate[$key] = ($byDate[$key] ?? 0) + 1; } $out = []; for ($i = 0; $i < $days; $i++) { $key = date('Y-m-d', $startTs + $i * 86400); $out[] = ['date' => $key, 'count' => $byDate[$key] ?? 0]; } return $out; } function qdb_analytics_overview(PDO $pdo, array $tokenRec): array { [$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl'); $stmt = $pdo->prepare("SELECT COUNT(*) FROM client cl WHERE $rbacClause"); $stmt->execute($rbacParams); $clientCount = (int)$stmt->fetchColumn(); $stmt = $pdo->prepare( "SELECT COUNT(DISTINCT cq.clientCode) FROM completed_questionnaire cq INNER JOIN client cl ON cl.clientCode = cq.clientCode WHERE $rbacClause" ); $stmt->execute($rbacParams); $clientsWithCompletions = (int)$stmt->fetchColumn(); $now = time(); $d7 = $now - 7 * 86400; $d30 = $now - 30 * 86400; $stmt = $pdo->prepare( "SELECT COUNT(*) FROM questionnaire_submission qs INNER JOIN client cl ON cl.clientCode = qs.clientCode WHERE $rbacClause AND qs.submittedAt >= :t" ); $stmt->execute(array_merge($rbacParams, [':t' => $d7])); $submissions7d = (int)$stmt->fetchColumn(); $stmt = $pdo->prepare( "SELECT COUNT(*) FROM questionnaire_submission qs INNER JOIN client cl ON cl.clientCode = qs.clientCode WHERE $rbacClause AND qs.submittedAt >= :t" ); $stmt->execute(array_merge($rbacParams, [':t' => $d30])); $submissions30d = (int)$stmt->fetchColumn(); $qnRows = $pdo->query( "SELECT questionnaireID, name, orderIndex FROM questionnaire WHERE state = 'active' ORDER BY orderIndex, name" )->fetchAll(PDO::FETCH_ASSOC); $perQuestionnaire = []; foreach ($qnRows as $qn) { $qnID = $qn['questionnaireID']; $stmt = $pdo->prepare( "SELECT COUNT(DISTINCT cq.clientCode) FROM completed_questionnaire cq INNER JOIN client cl ON cl.clientCode = cq.clientCode WHERE cq.questionnaireID = :qn AND $rbacClause" ); $stmt->execute(array_merge([':qn' => $qnID], $rbacParams)); $completed = (int)$stmt->fetchColumn(); $ratio = $clientCount > 0 ? round(100 * $completed / $clientCount, 1) : 0; $perQuestionnaire[] = [ 'questionnaireID' => $qnID, 'name' => $qn['name'], 'completedCount' => $completed, 'eligibleCount' => $clientCount, 'completionRatio' => $ratio, ]; } return [ 'clientCount' => $clientCount, 'clientsWithCompletions' => $clientsWithCompletions, 'submissionsLast7d' => $submissions7d, 'submissionsLast30d' => $submissions30d, 'questionnaires' => $perQuestionnaire, 'submissionsByDay' => qdb_analytics_submissions_by_day($pdo, $tokenRec, 14), 'scoringProfiles' => qdb_analytics_scoring_profiles($pdo, $tokenRec), ]; } /** * Band distribution and averages per active scoring profile (RBAC-scoped). * * @return list> */ function qdb_analytics_scoring_profiles(PDO $pdo, array $tokenRec): array { require_once __DIR__ . '/scoring.php'; [$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl'); $profiles = $pdo->query( "SELECT profileID, name, description, greenMin, greenMax, yellowMin, yellowMax, redMin, isActive FROM scoring_profile WHERE isActive = 1 ORDER BY name" )->fetchAll(PDO::FETCH_ASSOC); $out = []; foreach ($profiles as $p) { $profileID = $p['profileID']; $stmt = $pdo->prepare( "SELECT r.band, r.coachBand, r.weightedTotal FROM client_scoring_profile_result r INNER JOIN client cl ON cl.clientCode = r.clientCode WHERE r.profileID = :pid AND ($rbacClause)" ); $stmt->execute(array_merge([':pid' => $profileID], $rbacParams)); $computedBands = ['green' => 0, 'yellow' => 0, 'red' => 0]; $coachBands = ['green' => 0, 'yellow' => 0, 'red' => 0]; $pendingReview = 0; $totalSum = 0.0; $count = 0; foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { $calcBand = $row['band'] ?? ''; if (isset($computedBands[$calcBand])) { $computedBands[$calcBand]++; } $coachBand = trim((string)($row['coachBand'] ?? '')); if ($coachBand === '') { $pendingReview++; } elseif (isset($coachBands[$coachBand])) { $coachBands[$coachBand]++; } $totalSum += (float)$row['weightedTotal']; $count++; } $members = qdb_scoring_profile_members($pdo, $profileID); $bandRanges = qdb_normalize_scoring_bands($p); $out[] = [ 'profileID' => $profileID, 'name' => $p['name'], 'description' => $p['description'] ?? '', 'greenMin' => $bandRanges['greenMin'], 'greenMax' => $bandRanges['greenMax'], 'yellowMin' => $bandRanges['yellowMin'], 'yellowMax' => $bandRanges['yellowMax'], 'redMin' => $bandRanges['redMin'], 'questionnaires' => $members, 'clientCount' => $count, 'bands' => $computedBands, 'computedBands' => $computedBands, 'coachBands' => $coachBands, 'pendingReview' => $pendingReview, 'averageTotal' => $count > 0 ? round($totalSum / $count, 2) : null, ]; } return $out; } function qdb_analytics_stale_clients(PDO $pdo, array $tokenRec): array { [$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl'); $now = time(); $sql = " SELECT cl.clientCode, co.username AS coachUsername, co.coachID, lc.lastCompletedAt, lc.lastQuestionnaireID, qn.name AS lastQuestionnaireName, la.lastActivityAt, n.note AS followupNote FROM client cl INNER JOIN ( SELECT cq.clientCode, cq.questionnaireID AS lastQuestionnaireID, cq.completedAt AS lastCompletedAt FROM completed_questionnaire cq WHERE cq.completedAt IS NOT NULL AND cq.rowid = ( SELECT c2.rowid FROM completed_questionnaire c2 WHERE c2.clientCode = cq.clientCode AND c2.completedAt IS NOT NULL ORDER BY c2.completedAt DESC, c2.questionnaireID DESC LIMIT 1 ) ) lc ON lc.clientCode = cl.clientCode LEFT JOIN questionnaire qn ON qn.questionnaireID = lc.lastQuestionnaireID LEFT JOIN coach co ON co.coachID = cl.coachID LEFT JOIN ( SELECT qs.clientCode, MAX(qs.submittedAt) AS lastActivityAt FROM questionnaire_submission qs GROUP BY qs.clientCode ) la ON la.clientCode = cl.clientCode LEFT JOIN client_followup_note n ON n.clientCode = cl.clientCode WHERE $rbacClause ORDER BY COALESCE(la.lastActivityAt, 0) ASC, cl.clientCode ASC "; $stmt = $pdo->prepare($sql); $stmt->execute($rbacParams); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $out = []; foreach ($rows as $r) { $completedTs = $r['lastCompletedAt'] ? (int)$r['lastCompletedAt'] : null; $uploadTs = $r['lastActivityAt'] !== null ? (int)$r['lastActivityAt'] : null; $lastAt = null; if ($completedTs !== null && $uploadTs !== null) { $lastAt = max($completedTs, $uploadTs); } elseif ($completedTs !== null) { $lastAt = $completedTs; } else { $lastAt = $uploadTs; } $daysSince = $lastAt !== null ? (int)floor(($now - $lastAt) / 86400) : null; $out[] = [ 'clientCode' => $r['clientCode'], 'coachUsername' => $r['coachUsername'] ?? $r['coachID'], 'lastQuestionnaireID' => $r['lastQuestionnaireID'], 'lastQuestionnaireName' => $r['lastQuestionnaireName'] ?? $r['lastQuestionnaireID'], 'lastCompletedAt' => $completedTs ? date('Y-m-d H:i', $completedTs) : '', 'daysSinceLastActivity' => $daysSince, 'note' => $r['followupNote'] ?? '', ]; } return $out; } function qdb_analytics_set_followup_note(PDO $pdo, array $tokenRec, string $clientCode, string $note): void { $clientCode = trim($clientCode); if ($clientCode === '') { json_error('INVALID_FIELD', 'clientCode is required', 400); } [$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl'); $stmt = $pdo->prepare("SELECT 1 FROM client cl WHERE cl.clientCode = :cc AND ($rbacClause)"); $stmt->execute(array_merge([':cc' => $clientCode], $rbacParams)); if (!$stmt->fetchColumn()) { json_error('NOT_FOUND', 'Client not found or not authorized', 404); } $pdo->prepare( 'INSERT INTO client_followup_note (clientCode, note, updatedByUserID, updatedAt) VALUES (:cc, :n, :uid, :ts) ON CONFLICT(clientCode) DO UPDATE SET note = excluded.note, updatedByUserID = excluded.updatedByUserID, updatedAt = excluded.updatedAt' )->execute([ ':cc' => $clientCode, ':n' => $note, ':uid' => $tokenRec['userID'] ?? '', ':ts' => time(), ]); } function qdb_coach_activity_list(PDO $pdo, array $tokenRec): array { $role = $tokenRec['role'] ?? ''; $entityID = $tokenRec['entityID'] ?? ''; if ($role === 'supervisor') { $coachWhere = 'co.supervisorID = :eid'; $coachParams = [':eid' => $entityID]; } else { $coachWhere = '1=1'; $coachParams = []; } $now = time(); $d7 = $now - 7 * 86400; $d30 = $now - 30 * 86400; $sql = " SELECT co.coachID, co.username, sv.username AS supervisorUsername, (SELECT COUNT(*) FROM client c WHERE c.coachID = co.coachID AND COALESCE(c.archived, 0) = 0) AS clientCount, (SELECT COUNT(*) FROM questionnaire_submission qs INNER JOIN client c ON c.clientCode = qs.clientCode WHERE c.coachID = co.coachID) AS totalSubmissions, (SELECT COUNT(*) FROM questionnaire_submission qs INNER JOIN client c ON c.clientCode = qs.clientCode WHERE c.coachID = co.coachID AND qs.submittedAt >= :d7) AS submissions7d, (SELECT COUNT(*) FROM questionnaire_submission qs INNER JOIN client c ON c.clientCode = qs.clientCode WHERE c.coachID = co.coachID AND qs.submittedAt >= :d30) AS submissions30d, (SELECT MAX(qs.submittedAt) FROM questionnaire_submission qs INNER JOIN client c ON c.clientCode = qs.clientCode WHERE c.coachID = co.coachID) AS lastSubmissionAt FROM coach co LEFT JOIN supervisor sv ON sv.supervisorID = co.supervisorID WHERE $coachWhere ORDER BY co.username "; $stmt = $pdo->prepare($sql); $stmt->execute(array_merge([':d7' => $d7, ':d30' => $d30], $coachParams)); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $out = []; foreach ($rows as $r) { $last = $r['lastSubmissionAt'] !== null ? (int)$r['lastSubmissionAt'] : null; $out[] = [ 'coachID' => $r['coachID'], 'username' => $r['username'], 'supervisorUsername'=> $r['supervisorUsername'] ?? '', 'clientCount' => (int)$r['clientCount'], 'totalSubmissions' => (int)$r['totalSubmissions'], 'submissionsLast7d' => (int)$r['submissions7d'], 'submissionsLast30d'=> (int)$r['submissions30d'], 'lastSubmissionAt' => $last ? date('Y-m-d H:i', $last) : '', ]; } return $out; } function qdb_coach_recent_submissions( PDO $pdo, array $tokenRec, string $coachID, int $limit = 100, int $offset = 0 ): array { $coachID = trim($coachID); if ($coachID === '') { json_error('INVALID_FIELD', 'coachID is required', 400); } $role = $tokenRec['role'] ?? ''; $entityID = $tokenRec['entityID'] ?? ''; $chk = $pdo->prepare('SELECT coachID, supervisorID FROM coach WHERE coachID = :id'); $chk->execute([':id' => $coachID]); $coach = $chk->fetch(PDO::FETCH_ASSOC); if (!$coach) { json_error('NOT_FOUND', 'Counselor not found', 404); } if ($role === 'supervisor' && ($coach['supervisorID'] ?? '') !== $entityID) { json_error('FORBIDDEN', 'Not authorized for this coach', 403); } [$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl'); $limit = max(1, min($limit, 200)); $offset = max(0, $offset); $countSql = " SELECT COUNT(*) FROM questionnaire_submission qs INNER JOIN client cl ON cl.clientCode = qs.clientCode WHERE cl.coachID = :cid AND ($rbacClause) "; $countStmt = $pdo->prepare($countSql); $countStmt->execute(array_merge([':cid' => $coachID], $rbacParams)); $total = (int)$countStmt->fetchColumn(); $sql = " SELECT qs.submissionID, qs.version, qs.submittedAt, qs.submittedByRole, qs.clientCode, qs.questionnaireID, qn.name AS questionnaireName FROM questionnaire_submission qs INNER JOIN client cl ON cl.clientCode = qs.clientCode LEFT JOIN questionnaire qn ON qn.questionnaireID = qs.questionnaireID WHERE cl.coachID = :cid AND ($rbacClause) ORDER BY qs.submittedAt DESC LIMIT $limit OFFSET $offset "; $stmt = $pdo->prepare($sql); $stmt->execute(array_merge([':cid' => $coachID], $rbacParams)); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $submissions = array_map(static function ($r) { return [ 'submissionID' => $r['submissionID'], 'version' => (int)$r['version'], 'submittedAt' => $r['submittedAt'] ? date('Y-m-d H:i', (int)$r['submittedAt']) : '', 'submittedByRole' => $r['submittedByRole'] ?? '', 'clientCode' => $r['clientCode'], 'questionnaireID' => $r['questionnaireID'], 'questionnaireName'=> $r['questionnaireName'] ?? $r['questionnaireID'], ]; }, $rows); return [ 'submissions' => $submissions, 'total' => $total, 'limit' => $limit, 'offset' => $offset, ]; }