prepare( "SELECT cl.clientCode, cl.coachID, cl.archived, cl.archivedAt, co.username AS coachUsername, $noteSelect, CASE WHEN EXISTS ( SELECT 1 FROM completed_questionnaire cq WHERE cq.clientCode = cl.clientCode ) OR EXISTS ( SELECT 1 FROM questionnaire_submission qs WHERE qs.clientCode = cl.clientCode ) THEN 1 ELSE 0 END AS hasResponseData FROM client cl LEFT JOIN coach co ON co.coachID = cl.coachID $noteJoin WHERE $clause ORDER BY cl.archived ASC, hasResponseData DESC, cl.clientCode ASC" ); foreach ($params as $k => $v) $stmt->bindValue($k, $v); $stmt->execute(); $clients = $stmt->fetchAll(PDO::FETCH_ASSOC); require_once __DIR__ . '/../lib/submissions.php'; $scoringSummary = qdb_clients_scoring_summary_for_list($pdo, $tokenRec); foreach ($clients as &$client) { $client['note'] = (string)($client['note'] ?? ''); $client['scoringProfiles'] = qdb_client_scoring_dots_for_client( $client['clientCode'], $scoringSummary ); } unset($client); qdb_discard($tmpDb, $lockFp); json_success([ 'clients' => $clients, 'scoringProfiles' => $scoringSummary['profiles'], ]); } catch (Throwable $e) { qdb_handler_fail($e, 'clients', null, $tmpDb ?? null, $lockFp ?? null); } break; case 'POST': $body = read_json_body(); $coachID = trim($body['coachID'] ?? ''); $bulk = !empty($body['bulk']); if ($coachID === '') { json_error('MISSING_FIELDS', 'coachID is required', 400); } try { [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); if ($callerRole === 'supervisor') { $chkCoach = $pdo->prepare("SELECT coachID FROM coach WHERE coachID = :cid AND supervisorID = :sid"); $chkCoach->execute([':cid' => $coachID, ':sid' => $callerEntityID]); } else { $chkCoach = $pdo->prepare("SELECT coachID FROM coach WHERE coachID = :cid"); $chkCoach->execute([':cid' => $coachID]); } if (!$chkCoach->fetch()) { qdb_discard($tmpDb, $lockFp); json_error('NOT_FOUND', 'Counselor not found or not authorized', 404); } if ($bulk) { $fromCode = trim($body['fromCode'] ?? ''); $toCode = trim($body['toCode'] ?? ''); if ($fromCode === '' || $toCode === '') { qdb_discard($tmpDb, $lockFp); json_error('MISSING_FIELDS', 'fromCode and toCode are required for bulk creation', 400); } require_once __DIR__ . '/../lib/client_code_range.php'; try { $codes = qdb_expand_client_code_range($fromCode, $toCode); } catch (InvalidArgumentException $e) { qdb_discard($tmpDb, $lockFp); json_error('INVALID_RANGE', $e->getMessage(), 400); } $chk = $pdo->prepare("SELECT clientCode FROM client WHERE clientCode = :cc"); $insert = $pdo->prepare( "INSERT INTO client (clientCode, coachID, archived, archivedAt) VALUES (:cc, :cid, 0, 0)" ); $created = []; $skipped = []; foreach ($codes as $code) { $chk->execute([':cc' => $code]); if ($chk->fetch()) { $skipped[] = ['clientCode' => $code, 'reason' => 'duplicate']; continue; } $insert->execute([':cc' => $code, ':cid' => $coachID]); $created[] = $code; } qdb_save($tmpDb, $lockFp); json_success([ 'coachID' => $coachID, 'fromCode' => $fromCode, 'toCode' => $toCode, 'created' => $created, 'skipped' => $skipped, 'createdCount' => count($created), 'skippedCount' => count($skipped), ]); } $clientCode = trim($body['clientCode'] ?? ''); if ($clientCode === '') { qdb_discard($tmpDb, $lockFp); json_error('MISSING_FIELDS', 'clientCode is required', 400); } $chk = $pdo->prepare("SELECT clientCode FROM client WHERE clientCode = :cc"); $chk->execute([':cc' => $clientCode]); if ($chk->fetch()) { qdb_discard($tmpDb, $lockFp); json_error('DUPLICATE', 'Client code already exists', 409); } $pdo->prepare( "INSERT INTO client (clientCode, coachID, archived, archivedAt) VALUES (:cc, :cid, 0, 0)" )->execute([':cc' => $clientCode, ':cid' => $coachID]); qdb_save($tmpDb, $lockFp); json_success(['clientCode' => $clientCode, 'coachID' => $coachID]); } catch (Throwable $e) { qdb_handler_fail($e, 'clients', null, $tmpDb ?? null, $lockFp ?? null); } break; case 'PUT': $body = read_json_body(); $clientCode = trim((string)($body['clientCode'] ?? '')); $profileID = trim((string)($body['profileID'] ?? '')); $coachBand = trim((string)($body['coachBand'] ?? '')); if ($clientCode === '' || $profileID === '' || $coachBand === '') { json_error('MISSING_FIELDS', 'clientCode, profileID, and coachBand are required', 400); } $calculatedBand = trim((string)($body['calculatedBand'] ?? '')); $weightedTotal = isset($body['weightedTotal']) ? (float)$body['weightedTotal'] : null; try { [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); [$clause, $params] = rbac_client_filter($tokenRec, 'cl'); $chk = $pdo->prepare( "SELECT clientCode FROM client cl WHERE cl.clientCode = :cc AND ($clause)" ); $chk->execute(array_merge([':cc' => $clientCode], $params)); if (!$chk->fetch()) { qdb_discard($tmpDb, $lockFp); json_error('NOT_FOUND', 'Client not found or not authorized', 404); } require_once __DIR__ . '/../lib/scoring.php'; if (!qdb_is_valid_scoring_band($coachBand)) { qdb_discard($tmpDb, $lockFp); json_error('INVALID_FIELD', 'coachBand must be green, yellow, or red', 400); } $profile = qdb_save_coach_scoring_review( $pdo, $clientCode, $profileID, $coachBand, (string)($tokenRec['userID'] ?? ''), $weightedTotal, $calculatedBand !== '' ? $calculatedBand : null, ); qdb_save($tmpDb, $lockFp); json_success(['profile' => $profile]); } catch (InvalidArgumentException $e) { qdb_discard($tmpDb ?? null, $lockFp ?? null); json_error('INVALID_FIELD', $e->getMessage(), 400); } catch (RuntimeException $e) { qdb_discard($tmpDb ?? null, $lockFp ?? null); json_error('NOT_FOUND', $e->getMessage(), 404); } catch (Throwable $e) { qdb_handler_fail($e, 'clients', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null); } break; case 'PATCH': $body = read_json_body(); $clientCode = trim((string)($body['clientCode'] ?? '')); if ($clientCode === '' || !array_key_exists('archived', $body)) { json_error('MISSING_FIELDS', 'clientCode and archived are required', 400); } $archived = !empty($body['archived']) ? 1 : 0; try { [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); [$clause, $params] = rbac_client_filter($tokenRec, 'cl', 'all'); $chk = $pdo->prepare( "SELECT clientCode, archived FROM client cl WHERE cl.clientCode = :cc AND ($clause)" ); $chk->execute(array_merge([':cc' => $clientCode], $params)); $row = $chk->fetch(PDO::FETCH_ASSOC); if (!$row) { qdb_discard($tmpDb, $lockFp); json_error('NOT_FOUND', 'Client not found or not authorized', 404); } $archivedAt = $archived ? time() : 0; $pdo->prepare( 'UPDATE client SET archived = :a, archivedAt = :at WHERE clientCode = :cc' )->execute([':a' => $archived, ':at' => $archivedAt, ':cc' => $clientCode]); qdb_save($tmpDb, $lockFp); json_success([ 'clientCode' => $clientCode, 'archived' => $archived, 'archivedAt' => $archivedAt, ]); } catch (Throwable $e) { qdb_handler_fail($e, 'clients', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null); } break; case 'DELETE': $body = read_json_body(); $clientCode = trim($body['clientCode'] ?? ''); if ($clientCode === '') { json_error('MISSING_FIELDS', 'clientCode is required', 400); } try { [$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail(); // Verify the client exists and the caller can see it (RBAC) [$clause, $params] = rbac_client_filter($tokenRec, 'cl'); $chk = $pdo->prepare( "SELECT clientCode FROM client cl WHERE cl.clientCode = :cc AND ($clause)" ); $chk->execute(array_merge([':cc' => $clientCode], $params)); if (!$chk->fetch()) { qdb_discard($tmpDb, $lockFp); json_error('NOT_FOUND', 'Client not found or not authorized', 404); } require_once __DIR__ . '/../lib/submissions.php'; $pdo->beginTransaction(); qdb_delete_client_response_data($pdo, [$clientCode]); $pdo->prepare('DELETE FROM client WHERE clientCode = :cc') ->execute([':cc' => $clientCode]); $pdo->commit(); qdb_save($tmpDb, $lockFp); json_success([]); } catch (Throwable $e) { qdb_handler_fail($e, 'clients', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null); } break; default: json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405); }