This commit is contained in:
@ -281,7 +281,7 @@ function qdb_api_log_finish(): void
|
||||
}
|
||||
|
||||
$entry = [
|
||||
'ts' => gmdate('c'),
|
||||
'ts' => date('c'),
|
||||
'activity' => $activity,
|
||||
'method' => $ctx['method'] ?? '',
|
||||
'route' => $ctx['route'] ?? '',
|
||||
|
||||
106
lib/scoring.php
106
lib/scoring.php
@ -835,3 +835,109 @@ function qdb_set_coach_scoring_band(
|
||||
$row['profileID'] = $profileID;
|
||||
return qdb_scoring_review_row_to_api($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{profileID: string, name: string}>
|
||||
*/
|
||||
function qdb_active_scoring_profiles(PDO $pdo): array {
|
||||
$rows = $pdo->query(
|
||||
"SELECT profileID, name FROM scoring_profile WHERE isActive = 1 ORDER BY name"
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
return array_map(static fn(array $p) => [
|
||||
'profileID' => (string)$p['profileID'],
|
||||
'name' => (string)$p['name'],
|
||||
], $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* CSV / results table columns for active scoring profiles.
|
||||
*
|
||||
* @param list<array{profileID: string, name: string}> $profiles
|
||||
* @return list<array{profileID: string, field: string, header: string}>
|
||||
*/
|
||||
function qdb_scoring_export_column_defs(array $profiles): array {
|
||||
$cols = [];
|
||||
foreach ($profiles as $p) {
|
||||
$name = trim((string)($p['name'] ?? ''));
|
||||
$prefix = $name !== '' ? $name : (string)($p['profileID'] ?? '');
|
||||
$profileID = (string)($p['profileID'] ?? '');
|
||||
if ($profileID === '') {
|
||||
continue;
|
||||
}
|
||||
$cols[] = ['profileID' => $profileID, 'field' => 'calculated', 'header' => "{$prefix} calculated category"];
|
||||
$cols[] = ['profileID' => $profileID, 'field' => 'counselor', 'header' => "{$prefix} counselor category"];
|
||||
$cols[] = ['profileID' => $profileID, 'field' => 'override_by', 'header' => "{$prefix} override by"];
|
||||
$cols[] = ['profileID' => $profileID, 'field' => 'override_at', 'header' => "{$prefix} override at"];
|
||||
}
|
||||
return $cols;
|
||||
}
|
||||
|
||||
function qdb_scoring_export_cell_value(array $profileResult, string $field): string {
|
||||
$calc = (string)($profileResult['band'] ?? '');
|
||||
$coach = trim((string)($profileResult['coachBand'] ?? ''));
|
||||
$differs = $coach !== '' && $coach !== $calc;
|
||||
return match ($field) {
|
||||
'calculated' => $calc,
|
||||
'counselor' => $coach,
|
||||
'override_by' => $differs ? (string)($profileResult['coachReviewedByUsername'] ?? '') : '',
|
||||
'override_at' => $differs && !empty($profileResult['coachReviewedAt'])
|
||||
? date('Y-m-d H:i', (int)$profileResult['coachReviewedAt']) : '',
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{profileID: string, field: string, header: string}> $columnDefs
|
||||
* @param array<string, array<string, mixed>> $byProfile profileID => result row
|
||||
* @return array<string, string>
|
||||
*/
|
||||
function qdb_scoring_export_row_values(array $byProfile, array $columnDefs): array {
|
||||
$row = [];
|
||||
foreach ($columnDefs as $col) {
|
||||
$profileID = (string)($col['profileID'] ?? '');
|
||||
$result = $byProfile[$profileID] ?? null;
|
||||
$row[$col['header']] = is_array($result)
|
||||
? qdb_scoring_export_cell_value($result, (string)($col['field'] ?? ''))
|
||||
: '';
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scoring results for export, keyed by clientCode then profileID.
|
||||
*
|
||||
* @param list<string> $clientCodes
|
||||
* @return array<string, array<string, array<string, mixed>>>
|
||||
*/
|
||||
function qdb_scoring_export_results_map(PDO $pdo, array $clientCodes, array $tokenRec): array {
|
||||
if ($clientCodes === []) {
|
||||
return [];
|
||||
}
|
||||
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
|
||||
$placeholders = implode(',', array_fill(0, count($clientCodes), '?'));
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT r.clientCode, r.profileID, r.band, r.coachBand, r.coachReviewedAt, r.coachReviewedByUserID,
|
||||
u.username AS coachReviewedByUsername
|
||||
FROM client_scoring_profile_result r
|
||||
INNER JOIN scoring_profile sp ON sp.profileID = r.profileID AND sp.isActive = 1
|
||||
INNER JOIN client cl ON cl.clientCode = r.clientCode
|
||||
LEFT JOIN users u ON u.userID = r.coachReviewedByUserID
|
||||
WHERE r.clientCode IN ($placeholders) AND ($rbacClause)"
|
||||
);
|
||||
$stmt->execute(array_merge(array_values($clientCodes), $rbacParams));
|
||||
|
||||
$map = [];
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$cc = (string)$row['clientCode'];
|
||||
$map[$cc][(string)$row['profileID']] = $row;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
function qdb_scoring_export_csv_headers(PDO $pdo): array {
|
||||
$profiles = qdb_active_scoring_profiles($pdo);
|
||||
return array_column(qdb_scoring_export_column_defs($profiles), 'header');
|
||||
}
|
||||
|
||||
@ -43,6 +43,9 @@ function qdb_display_context_from_manifest(PDO $pdo, array $manifest): array {
|
||||
),
|
||||
];
|
||||
}
|
||||
if (qdb_question_is_results_excluded($qRow)) {
|
||||
continue;
|
||||
}
|
||||
$questions[] = $qRow;
|
||||
|
||||
$cfg = json_decode($qRow['configJson'] ?? '{}', true) ?: [];
|
||||
@ -293,7 +296,10 @@ function qdb_export_all_versions_rows(
|
||||
array $optionTextMap,
|
||||
array $tokenRec
|
||||
): array {
|
||||
require_once __DIR__ . '/scoring.php';
|
||||
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
|
||||
$scoringProfiles = qdb_active_scoring_profiles($pdo);
|
||||
$scoringColumnDefs = qdb_scoring_export_column_defs($scoringProfiles);
|
||||
|
||||
$sql = "
|
||||
SELECT qs.submissionID, qs.version, qs.submittedAt, qs.submittedByUserID, qs.submittedByRole,
|
||||
@ -315,6 +321,9 @@ function qdb_export_all_versions_rows(
|
||||
$stmt->execute($params);
|
||||
$submissions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$clientCodes = array_values(array_unique(array_column($submissions, 'clientCode')));
|
||||
$scoringByClient = qdb_scoring_export_results_map($pdo, $clientCodes, $tokenRec);
|
||||
|
||||
$defaultQuestionIDs = array_column($questions, 'questionID');
|
||||
|
||||
$userMap = [];
|
||||
@ -353,6 +362,14 @@ function qdb_export_all_versions_rows(
|
||||
'completedAt' => $s['submissionCompletedAt'] ? date('Y-m-d H:i', (int)$s['submissionCompletedAt']) : '',
|
||||
];
|
||||
|
||||
$row = array_merge(
|
||||
$row,
|
||||
qdb_scoring_export_row_values(
|
||||
$scoringByClient[$s['clientCode']] ?? [],
|
||||
$scoringColumnDefs
|
||||
)
|
||||
);
|
||||
|
||||
$answerMap = qdb_load_client_answer_map(
|
||||
$pdo,
|
||||
$s['clientCode'],
|
||||
@ -413,8 +430,11 @@ function qdb_export_current_rows(
|
||||
array $optionTextMap,
|
||||
array $tokenRec
|
||||
): array {
|
||||
require_once __DIR__ . '/scoring.php';
|
||||
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
|
||||
$questionIDs = array_column($questions, 'questionID');
|
||||
$scoringProfiles = qdb_active_scoring_profiles($pdo);
|
||||
$scoringColumnDefs = qdb_scoring_export_column_defs($scoringProfiles);
|
||||
|
||||
$sql = "
|
||||
SELECT cl.clientCode, cl.coachID,
|
||||
@ -433,6 +453,9 @@ function qdb_export_current_rows(
|
||||
$cStmt->execute($params);
|
||||
$clients = $cStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$clientCodes = array_column($clients, 'clientCode');
|
||||
$scoringByClient = qdb_scoring_export_results_map($pdo, $clientCodes, $tokenRec);
|
||||
|
||||
$qPlaceholders = !empty($questionIDs) ? implode(',', array_fill(0, count($questionIDs), '?')) : "'__none__'";
|
||||
$answerStmt = $pdo->prepare("
|
||||
SELECT questionID, answerOptionID, freeTextValue, numericValue
|
||||
@ -452,6 +475,14 @@ function qdb_export_current_rows(
|
||||
'completedAt' => $c['completedAt'] ? date('Y-m-d H:i', (int)$c['completedAt']) : '',
|
||||
];
|
||||
|
||||
$row = array_merge(
|
||||
$row,
|
||||
qdb_scoring_export_row_values(
|
||||
$scoringByClient[$c['clientCode']] ?? [],
|
||||
$scoringColumnDefs
|
||||
)
|
||||
);
|
||||
|
||||
$bindParams = array_merge([$c['clientCode']], $questionIDs);
|
||||
$answerStmt->execute($bindParams);
|
||||
$answerMap = [];
|
||||
@ -514,19 +545,23 @@ function qdb_export_rows_to_csv_string(array $rows, array $fallbackHeader = []):
|
||||
return $csv !== false ? $csv : '';
|
||||
}
|
||||
|
||||
function qdb_export_current_csv_fallback_header(array $resultColumns): array {
|
||||
function qdb_export_current_csv_fallback_header(PDO $pdo, array $resultColumns): array {
|
||||
require_once __DIR__ . '/scoring.php';
|
||||
$header = ['clientCode', 'coach', 'supervisor', 'status', 'sumPoints', 'startedAt', 'completedAt'];
|
||||
$header = array_merge($header, qdb_scoring_export_csv_headers($pdo));
|
||||
foreach ($resultColumns as $col) {
|
||||
$header[] = $col['header'];
|
||||
}
|
||||
return $header;
|
||||
}
|
||||
|
||||
function qdb_export_all_versions_csv_fallback_header(array $resultColumns): array {
|
||||
function qdb_export_all_versions_csv_fallback_header(PDO $pdo, array $resultColumns): array {
|
||||
require_once __DIR__ . '/scoring.php';
|
||||
$header = [
|
||||
'submissionID', 'version', 'submittedAt', 'submittedByRole', 'submittedBy',
|
||||
'clientCode', 'coach', 'supervisor', 'status', 'sumPoints', 'startedAt', 'completedAt',
|
||||
];
|
||||
$header = array_merge($header, qdb_scoring_export_csv_headers($pdo));
|
||||
foreach ($resultColumns as $col) {
|
||||
$header[] = $col['header'];
|
||||
}
|
||||
@ -566,11 +601,11 @@ function qdb_build_server_export_zip(PDO $pdo, array $tokenRec, bool $allVersion
|
||||
|
||||
if ($allVersions) {
|
||||
$rows = qdb_export_all_versions_rows($pdo, $qnID, $questions, $resultColumns, $optionTextMap, $tokenRec);
|
||||
$fallback = qdb_export_all_versions_csv_fallback_header($resultColumns);
|
||||
$fallback = qdb_export_all_versions_csv_fallback_header($pdo, $resultColumns);
|
||||
$base = qdb_export_safe_basename($qn['name']) . '_all_versions.csv';
|
||||
} else {
|
||||
$rows = qdb_export_current_rows($pdo, $qnID, $questions, $resultColumns, $optionTextMap, $tokenRec);
|
||||
$fallback = qdb_export_current_csv_fallback_header($resultColumns);
|
||||
$fallback = qdb_export_current_csv_fallback_header($pdo, $resultColumns);
|
||||
$ver = preg_replace('/[^a-zA-Z0-9_.-]+/', '_', (string)($qn['version'] ?? ''));
|
||||
$base = qdb_export_safe_basename($qn['name']) . ($ver !== '' ? "_v{$ver}" : '') . '.csv';
|
||||
}
|
||||
@ -593,7 +628,7 @@ function qdb_build_server_export_zip(PDO $pdo, array $tokenRec, bool $allVersion
|
||||
* Delete uploads, answers, and completions for client codes (does not delete client rows).
|
||||
*
|
||||
* @param list<string> $clientCodes
|
||||
* @return array{submissions: int, followup_notes: int, client_answers: int, completed_questionnaires: int}
|
||||
* @return array{submissions: int, followup_notes: int, client_answers: int, completed_questionnaires: int, scoring_results: int}
|
||||
*/
|
||||
function qdb_delete_client_response_data(PDO $pdo, array $clientCodes): array {
|
||||
$clientCodes = array_values(array_unique(array_filter(
|
||||
@ -605,6 +640,7 @@ function qdb_delete_client_response_data(PDO $pdo, array $clientCodes): array {
|
||||
'followup_notes' => 0,
|
||||
'client_answers' => 0,
|
||||
'completed_questionnaires' => 0,
|
||||
'scoring_results' => 0,
|
||||
];
|
||||
if ($clientCodes === []) {
|
||||
return $deleted;
|
||||
@ -628,6 +664,11 @@ function qdb_delete_client_response_data(PDO $pdo, array $clientCodes): array {
|
||||
$stmt = $pdo->prepare("DELETE FROM completed_questionnaire WHERE clientCode IN ($ph)");
|
||||
$stmt->execute($chunk);
|
||||
$deleted['completed_questionnaires'] += $stmt->rowCount();
|
||||
if (qdb_table_exists($pdo, 'client_scoring_profile_result')) {
|
||||
$stmt = $pdo->prepare("DELETE FROM client_scoring_profile_result WHERE clientCode IN ($ph)");
|
||||
$stmt->execute($chunk);
|
||||
$deleted['scoring_results'] += $stmt->rowCount();
|
||||
}
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
@ -893,9 +934,11 @@ function qdb_client_scoring_profile_results(PDO $pdo, string $clientCode): array
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT r.profileID, r.weightedTotal, r.band, r.computedAt, r.questionnaireSnapshot,
|
||||
r.coachBand, r.coachReviewedAt, r.coachReviewedByUserID,
|
||||
u.username AS coachReviewedByUsername,
|
||||
sp.name, sp.greenMin, sp.greenMax, sp.yellowMin, sp.yellowMax, sp.redMin
|
||||
FROM client_scoring_profile_result r
|
||||
JOIN scoring_profile sp ON sp.profileID = r.profileID
|
||||
LEFT JOIN users u ON u.userID = r.coachReviewedByUserID
|
||||
WHERE r.clientCode = :cc
|
||||
ORDER BY sp.name'
|
||||
);
|
||||
@ -904,6 +947,8 @@ function qdb_client_scoring_profile_results(PDO $pdo, string $clientCode): array
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$bands = qdb_normalize_scoring_bands($row);
|
||||
$coachBand = trim((string)($row['coachBand'] ?? ''));
|
||||
$calcBand = (string)$row['band'];
|
||||
$coachDiffers = $coachBand !== '' && $coachBand !== $calcBand;
|
||||
$out[] = [
|
||||
'profileID' => $row['profileID'],
|
||||
'name' => $row['name'],
|
||||
@ -913,6 +958,7 @@ function qdb_client_scoring_profile_results(PDO $pdo, string $clientCode): array
|
||||
'coachBand' => $coachBand !== '' ? $coachBand : null,
|
||||
'effectiveBand' => qdb_effective_scoring_band($row),
|
||||
'pendingReview' => $coachBand === '',
|
||||
'coachOverride' => $coachDiffers,
|
||||
'greenMin' => $bands['greenMin'],
|
||||
'greenMax' => $bands['greenMax'],
|
||||
'yellowMin' => $bands['yellowMin'],
|
||||
@ -921,6 +967,8 @@ function qdb_client_scoring_profile_results(PDO $pdo, string $clientCode): array
|
||||
'computedAt' => $row['computedAt'] ? date('Y-m-d H:i', (int)$row['computedAt']) : '',
|
||||
'coachReviewedAt' => !empty($row['coachReviewedAt'])
|
||||
? date('Y-m-d H:i', (int)$row['coachReviewedAt']) : '',
|
||||
'coachReviewedByUsername' => $coachDiffers
|
||||
? (string)($row['coachReviewedByUsername'] ?? '') : '',
|
||||
'snapshot' => json_decode($row['questionnaireSnapshot'] ?? '{}', true) ?: [],
|
||||
];
|
||||
}
|
||||
@ -945,10 +993,13 @@ function qdb_clients_scoring_summary_for_list(PDO $pdo, array $tokenRec): array
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT r.clientCode, r.profileID, r.band, r.coachBand, r.weightedTotal, sp.name
|
||||
"SELECT r.clientCode, r.profileID, r.band, r.coachBand, r.weightedTotal,
|
||||
r.coachReviewedAt, r.coachReviewedByUserID, u.username AS coachReviewedByUsername,
|
||||
sp.name
|
||||
FROM client_scoring_profile_result r
|
||||
INNER JOIN scoring_profile sp ON sp.profileID = r.profileID AND sp.isActive = 1
|
||||
INNER JOIN client cl ON cl.clientCode = r.clientCode
|
||||
LEFT JOIN users u ON u.userID = r.coachReviewedByUserID
|
||||
WHERE ($rbacClause)"
|
||||
);
|
||||
$stmt->execute($rbacParams);
|
||||
@ -957,14 +1008,21 @@ function qdb_clients_scoring_summary_for_list(PDO $pdo, array $tokenRec): array
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$cc = (string)$row['clientCode'];
|
||||
$coachBand = trim((string)($row['coachBand'] ?? ''));
|
||||
$calcBand = (string)$row['band'];
|
||||
$coachDiffers = $coachBand !== '' && $coachBand !== $calcBand;
|
||||
$byClient[$cc][$row['profileID']] = [
|
||||
'band' => (string)$row['band'],
|
||||
'calculatedBand' => (string)$row['band'],
|
||||
'band' => $calcBand,
|
||||
'calculatedBand' => $calcBand,
|
||||
'coachBand' => $coachBand !== '' ? $coachBand : null,
|
||||
'effectiveBand' => qdb_effective_scoring_band($row),
|
||||
'pendingReview' => $coachBand === '',
|
||||
'coachOverride' => $coachDiffers,
|
||||
'weightedTotal' => (float)$row['weightedTotal'],
|
||||
'name' => (string)$row['name'],
|
||||
'coachReviewedAt' => !empty($row['coachReviewedAt'])
|
||||
? date('Y-m-d H:i', (int)$row['coachReviewedAt']) : '',
|
||||
'coachReviewedByUsername' => $coachDiffers
|
||||
? (string)($row['coachReviewedByUsername'] ?? '') : '',
|
||||
];
|
||||
}
|
||||
|
||||
@ -992,6 +1050,9 @@ function qdb_client_scoring_dots_for_client(string $clientCode, array $summary):
|
||||
'coachBand' => $result['coachBand'] ?? null,
|
||||
'effectiveBand' => $result['effectiveBand'] ?? null,
|
||||
'pendingReview' => $result['pendingReview'] ?? false,
|
||||
'coachOverride' => $result['coachOverride'] ?? false,
|
||||
'coachReviewedAt' => $result['coachReviewedAt'] ?? '',
|
||||
'coachReviewedByUsername' => $result['coachReviewedByUsername'] ?? '',
|
||||
'weightedTotal' => $result !== null ? $result['weightedTotal'] : null,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user