added submission history, data insights, download all functionality
This commit is contained in:
304
lib/analytics.php
Normal file
304
lib/analytics.php
Normal file
@ -0,0 +1,304 @@
|
||||
<?php
|
||||
/**
|
||||
* Analytics queries for Insights dashboard (RBAC-scoped).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Daily upload counts for the last N calendar days (zeros for quiet days).
|
||||
*
|
||||
* @return list<array{date: string, count: int}>
|
||||
*/
|
||||
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');
|
||||
|
||||
$today = strtotime('today');
|
||||
$startTs = $today - ($days - 1) * 86400;
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT strftime('%Y-%m-%d', qs.submittedAt, 'unixepoch') AS d, COUNT(*) AS cnt
|
||||
FROM questionnaire_submission qs
|
||||
INNER JOIN client cl ON cl.clientCode = qs.clientCode
|
||||
WHERE $rbacClause AND qs.submittedAt >= :start
|
||||
GROUP BY d
|
||||
ORDER BY d"
|
||||
);
|
||||
$stmt->execute(array_merge($rbacParams, [':start' => $startTs]));
|
||||
$byDate = [];
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$byDate[$row['d']] = (int)$row['cnt'];
|
||||
}
|
||||
|
||||
$out = [];
|
||||
for ($i = 0; $i < $days; $i++) {
|
||||
$ts = $startTs + $i * 86400;
|
||||
$key = date('Y-m-d', $ts);
|
||||
$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),
|
||||
];
|
||||
}
|
||||
|
||||
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,
|
||||
fc.firstCompletedAt,
|
||||
fc.firstQuestionnaireID,
|
||||
qn.name AS firstQuestionnaireName,
|
||||
la.lastActivityAt,
|
||||
n.note AS followupNote
|
||||
FROM client cl
|
||||
INNER JOIN (
|
||||
SELECT cq.clientCode, cq.questionnaireID AS firstQuestionnaireID, cq.completedAt AS firstCompletedAt
|
||||
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 ASC, c2.questionnaireID ASC
|
||||
LIMIT 1
|
||||
)
|
||||
) fc ON fc.clientCode = cl.clientCode
|
||||
LEFT JOIN questionnaire qn ON qn.questionnaireID = fc.firstQuestionnaireID
|
||||
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) {
|
||||
$lastAt = $r['lastActivityAt'] !== null ? (int)$r['lastActivityAt'] : null;
|
||||
$daysSince = $lastAt !== null
|
||||
? (int)floor(($now - $lastAt) / 86400)
|
||||
: null;
|
||||
$out[] = [
|
||||
'clientCode' => $r['clientCode'],
|
||||
'coachUsername' => $r['coachUsername'] ?? $r['coachID'],
|
||||
'firstQuestionnaireID' => $r['firstQuestionnaireID'],
|
||||
'firstQuestionnaireName'=> $r['firstQuestionnaireName'] ?? $r['firstQuestionnaireID'],
|
||||
'firstCompletedAt' => $r['firstCompletedAt']
|
||||
? date('Y-m-d H:i', (int)$r['firstCompletedAt']) : '',
|
||||
'lastActivityAt' => $lastAt ? date('Y-m-d H:i', $lastAt) : '',
|
||||
'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) 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 = 25): 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', 'Coach 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, 100));
|
||||
|
||||
$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
|
||||
";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute(array_merge([':cid' => $coachID], $rbacParams));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return 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);
|
||||
}
|
||||
Reference in New Issue
Block a user