$profile]); } json_success(['profiles' => qdb_list_scoring_profiles($pdo)]); qdb_discard($tmpDb, $lockFp); } catch (Throwable $e) { qdb_handler_fail($e, 'List scoring profiles', null, $tmpDb ?? null, $lockFp ?? null); } break; case 'POST': require_role(['admin', 'supervisor'], $tokenRec); $body = read_json_body(); $name = trim($body['name'] ?? ''); if ($name === '') { json_error('BAD_REQUEST', 'name is required', 400); } $bands = qdb_parse_scoring_bands_body($body); $bandErr = qdb_validate_scoring_bands($bands); if ($bandErr !== null) { json_error('INVALID_FIELD', $bandErr, 400); } $members = qdb_parse_profile_questionnaires($body['questionnaires'] ?? []); if ($members === []) { json_error('INVALID_FIELD', 'At least one questionnaire is required', 400); } $processCompleteBands = qdb_parse_process_complete_bands($body['processCompleteBands'] ?? []); try { [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); $profileID = bin2hex(random_bytes(16)); $now = time(); $pdo->prepare( 'INSERT INTO scoring_profile (profileID, name, description, isActive, greenMin, greenMax, yellowMin, yellowMax, redMin, processCompleteBands, createdAt, updatedAt) VALUES (:id, :name, :desc, :act, :gmin, :gmax, :ymin, :ymax, :rmin, :pcb, :ca, :ua)' )->execute([ ':id' => $profileID, ':name' => $name, ':desc' => trim($body['description'] ?? ''), ':act' => !empty($body['isActive']) ? 1 : 0, ':gmin' => $bands['greenMin'], ':gmax' => $bands['greenMax'], ':ymin' => $bands['yellowMin'], ':ymax' => $bands['yellowMax'], ':rmin' => $bands['redMin'], ':pcb' => qdb_encode_process_complete_bands($processCompleteBands), ':ca' => $now, ':ua' => $now, ]); qdb_sync_profile_questionnaires($pdo, $profileID, $members); qdb_recompute_all_profiles_after_change($pdo); qdb_save($tmpDb, $lockFp); json_success(['profile' => qdb_get_scoring_profile($pdo, $profileID)]); } catch (Throwable $e) { qdb_handler_fail($e, 'Create scoring profile', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null); } break; case 'PUT': require_role(['admin', 'supervisor'], $tokenRec); $body = read_json_body(); $profileID = trim($body['profileID'] ?? ''); if ($profileID === '') { json_error('BAD_REQUEST', 'profileID is required', 400); } try { [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); $existing = qdb_get_scoring_profile($pdo, $profileID); if (!$existing) { qdb_discard($tmpDb, $lockFp); json_error('NOT_FOUND', 'Scoring profile not found', 404); } $name = trim($body['name'] ?? $existing['name']); $bands = qdb_normalize_scoring_bands(array_merge($existing, $body)); $bandErr = qdb_validate_scoring_bands($bands); if ($bandErr !== null) { qdb_discard($tmpDb, $lockFp); json_error('INVALID_FIELD', $bandErr, 400); } $members = isset($body['questionnaires']) ? qdb_parse_profile_questionnaires($body['questionnaires']) : null; if ($members !== null && $members === []) { qdb_discard($tmpDb, $lockFp); json_error('INVALID_FIELD', 'At least one questionnaire is required', 400); } $processCompleteBands = array_key_exists('processCompleteBands', $body) ? qdb_parse_process_complete_bands($body['processCompleteBands']) : qdb_row_process_complete_bands($existing); $pdo->prepare( 'UPDATE scoring_profile SET name = :name, description = :desc, isActive = :act, greenMin = :gmin, greenMax = :gmax, yellowMin = :ymin, yellowMax = :ymax, redMin = :rmin, processCompleteBands = :pcb, updatedAt = :ua WHERE profileID = :id' )->execute([ ':name' => $name, ':desc' => trim($body['description'] ?? $existing['description']), ':act' => isset($body['isActive']) ? (!empty($body['isActive']) ? 1 : 0) : $existing['isActive'], ':gmin' => $bands['greenMin'], ':gmax' => $bands['greenMax'], ':ymin' => $bands['yellowMin'], ':ymax' => $bands['yellowMax'], ':rmin' => $bands['redMin'], ':pcb' => qdb_encode_process_complete_bands($processCompleteBands), ':ua' => time(), ':id' => $profileID, ]); if ($members !== null) { qdb_sync_profile_questionnaires($pdo, $profileID, $members); } qdb_recompute_all_profiles_after_change($pdo, $profileID); qdb_save($tmpDb, $lockFp); json_success(['profile' => qdb_get_scoring_profile($pdo, $profileID)]); } catch (Throwable $e) { qdb_handler_fail($e, 'Update scoring profile', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null); } break; case 'DELETE': require_role(['admin', 'supervisor'], $tokenRec); $body = read_json_body(); $profileID = trim($body['profileID'] ?? ''); if ($profileID === '') { json_error('BAD_REQUEST', 'profileID is required', 400); } try { [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); $pdo->prepare('DELETE FROM scoring_profile WHERE profileID = :id')->execute([':id' => $profileID]); qdb_save($tmpDb, $lockFp); json_success(['deleted' => true]); } catch (Throwable $e) { qdb_handler_fail($e, 'Delete scoring profile', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null); } break; default: json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405); } /** * @return list */ function qdb_parse_profile_questionnaires(array $rows): array { $out = []; $idx = 0; foreach ($rows as $row) { if (!is_array($row)) { continue; } $qnID = trim((string)($row['questionnaireID'] ?? '')); if ($qnID === '') { continue; } $weight = (float)($row['weight'] ?? 1.0); if ($weight <= 0) { $weight = 1.0; } $out[] = [ 'questionnaireID' => $qnID, 'weight' => $weight, 'orderIndex' => (int)($row['orderIndex'] ?? $idx), ]; $idx++; } return $out; } function qdb_sync_profile_questionnaires(PDO $pdo, string $profileID, array $members): void { $pdo->prepare('DELETE FROM scoring_profile_questionnaire WHERE profileID = :pid') ->execute([':pid' => $profileID]); $ins = $pdo->prepare( 'INSERT INTO scoring_profile_questionnaire (profileID, questionnaireID, weight, orderIndex) VALUES (:pid, :qn, :w, :o)' ); foreach ($members as $m) { $ins->execute([ ':pid' => $profileID, ':qn' => $m['questionnaireID'], ':w' => $m['weight'], ':o' => $m['orderIndex'], ]); } } function qdb_recompute_all_profiles_after_change(PDO $pdo, ?string $profileID = null): void { $clients = $pdo->query('SELECT DISTINCT clientCode FROM completed_questionnaire WHERE status = \'completed\'') ->fetchAll(PDO::FETCH_COLUMN); foreach ($clients as $clientCode) { qdb_recompute_profile_scores_for_client($pdo, (string)$clientCode); } }