This commit is contained in:
@ -64,43 +64,6 @@ case 'GET':
|
|||||||
case 'POST':
|
case 'POST':
|
||||||
$body = read_json_body();
|
$body = read_json_body();
|
||||||
|
|
||||||
if (($body['action'] ?? '') === 'reset') {
|
|
||||||
$clientCode = trim((string)($body['clientCode'] ?? ''));
|
|
||||||
if ($clientCode === '') {
|
|
||||||
json_error('MISSING_FIELDS', 'clientCode is required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
|
|
||||||
|
|
||||||
[$clause, $params] = rbac_client_filter($tokenRec, 'cl', 'all');
|
|
||||||
$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();
|
|
||||||
$cleared = qdb_delete_client_response_data($pdo, [$clientCode]);
|
|
||||||
$pdo->commit();
|
|
||||||
|
|
||||||
qdb_save($tmpDb, $lockFp);
|
|
||||||
json_success([
|
|
||||||
'clientCode' => $clientCode,
|
|
||||||
'reset' => true,
|
|
||||||
'cleared' => $cleared,
|
|
||||||
]);
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
qdb_handler_fail($e, 'clients', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$coachID = trim($body['coachID'] ?? '');
|
$coachID = trim($body['coachID'] ?? '');
|
||||||
$bulk = !empty($body['bulk']);
|
$bulk = !empty($body['bulk']);
|
||||||
|
|
||||||
|
|||||||
@ -91,43 +91,6 @@ final class ClientsLifecycleTest extends QdbTestCase
|
|||||||
$this->assertContains($code, array_column($activeAgain['data']['clients'], 'clientCode'));
|
$this->assertContains($code, array_column($activeAgain['data']['clients'], 'clientCode'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testAdminResetsClient(): void
|
|
||||||
{
|
|
||||||
$this->submitFixtureQuestionnaire();
|
|
||||||
$token = $this->api()->loginWebAndGetToken(
|
|
||||||
DatabaseSeeder::ADMIN_USERNAME,
|
|
||||||
DatabaseSeeder::PASSWORD
|
|
||||||
)['token'];
|
|
||||||
$code = $this->fixture()->clientCode;
|
|
||||||
|
|
||||||
$reset = $this->api()->withToken($token, 'POST', 'clients', [
|
|
||||||
'action' => 'reset',
|
|
||||||
'clientCode' => $code,
|
|
||||||
]);
|
|
||||||
$this->assertApiOk($reset);
|
|
||||||
$this->assertTrue($reset['data']['reset'] ?? false);
|
|
||||||
|
|
||||||
$detail = $this->api()->withToken($token, 'GET', 'clients', null, [
|
|
||||||
'clientCode' => $code,
|
|
||||||
'detail' => '1',
|
|
||||||
]);
|
|
||||||
$this->assertApiOk($detail);
|
|
||||||
$this->assertSame($code, $detail['data']['client']['clientCode'] ?? '');
|
|
||||||
$this->assertSame([], $detail['data']['questionnaires'] ?? null);
|
|
||||||
|
|
||||||
$list = $this->api()->withToken($token, 'GET', 'clients');
|
|
||||||
$this->assertApiOk($list);
|
|
||||||
$row = null;
|
|
||||||
foreach ($list['data']['clients'] as $client) {
|
|
||||||
if (($client['clientCode'] ?? '') === $code) {
|
|
||||||
$row = $client;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$this->assertNotNull($row);
|
|
||||||
$this->assertSame(0, (int)($row['hasResponseData'] ?? 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testAdminDeletesClient(): void
|
public function testAdminDeletesClient(): void
|
||||||
{
|
{
|
||||||
$token = $this->api()->loginWebAndGetToken(
|
$token = $this->api()->loginWebAndGetToken(
|
||||||
|
|||||||
@ -34,19 +34,15 @@ export async function confirmAndPatchClientArchive(clientCode, archived) {
|
|||||||
* @param {string} opts.clientCode Escaped client code for data-code attributes
|
* @param {string} opts.clientCode Escaped client code for data-code attributes
|
||||||
* @param {boolean} opts.archived
|
* @param {boolean} opts.archived
|
||||||
* @param {boolean} [opts.showDelete]
|
* @param {boolean} [opts.showDelete]
|
||||||
* @param {boolean} [opts.showReset]
|
|
||||||
* @param {string} [opts.rawCode] Unescaped code for button labels (optional)
|
* @param {string} [opts.rawCode] Unescaped code for button labels (optional)
|
||||||
*/
|
*/
|
||||||
export function clientTableActionsHTML({ clientCode, archived, showDelete = false, showReset = false }) {
|
export function clientTableActionsHTML({ clientCode, archived, showDelete = false }) {
|
||||||
const archiveBtn = archived
|
const archiveBtn = archived
|
||||||
? `<button type="button" class="btn btn-sm restore-client-btn" data-code="${clientCode}" title="Restore to active lists">Restore</button>`
|
? `<button type="button" class="btn btn-sm restore-client-btn" data-code="${clientCode}" title="Restore to active lists">Restore</button>`
|
||||||
: `<button type="button" class="btn btn-sm archive-client-btn" data-code="${clientCode}" title="Mark as finished and hide from lists">Archive</button>`;
|
: `<button type="button" class="btn btn-sm archive-client-btn" data-code="${clientCode}" title="Mark as finished and hide from lists">Archive</button>`;
|
||||||
const resetBtn = showReset
|
|
||||||
? `<button type="button" class="btn btn-sm reset-client-btn" data-code="${clientCode}" title="Clear answers, uploads, and scores; keep client code and counselor">Reset</button>`
|
|
||||||
: '';
|
|
||||||
const deleteBtn = showDelete
|
const deleteBtn = showDelete
|
||||||
? `<button type="button" class="btn btn-sm btn-danger delete-client-btn" data-code="${clientCode}" title="Permanently delete client and all data">Delete</button>`
|
? `<button type="button" class="btn btn-sm btn-danger delete-client-btn" data-code="${clientCode}" title="Permanently delete client and all data">Delete</button>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return `<div class="table-row-actions" role="group" aria-label="Client actions">${archiveBtn}${resetBtn}${deleteBtn}</div>`;
|
return `<div class="table-row-actions" role="group" aria-label="Client actions">${archiveBtn}${deleteBtn}</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -197,9 +197,6 @@ function renderClientTableBody() {
|
|||||||
body.querySelectorAll('.delete-client-btn').forEach(btn => {
|
body.querySelectorAll('.delete-client-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => deleteClient(btn.dataset.code));
|
btn.addEventListener('click', () => deleteClient(btn.dataset.code));
|
||||||
});
|
});
|
||||||
body.querySelectorAll('.reset-client-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', () => resetClient(btn.dataset.code));
|
|
||||||
});
|
|
||||||
body.querySelectorAll('.archive-client-btn').forEach(btn => {
|
body.querySelectorAll('.archive-client-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => setClientArchived(btn.dataset.code, true));
|
btn.addEventListener('click', () => setClientArchived(btn.dataset.code, true));
|
||||||
});
|
});
|
||||||
@ -741,7 +738,6 @@ function clientRowHTML(c) {
|
|||||||
clientCode: esc(c.clientCode),
|
clientCode: esc(c.clientCode),
|
||||||
archived,
|
archived,
|
||||||
showDelete: true,
|
showDelete: true,
|
||||||
showReset: clientHasResponseData(c),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@ -828,44 +824,6 @@ async function setClientArchived(clientCode, archived) {
|
|||||||
else renderClientTableBody();
|
else renderClientTableBody();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetClient(clientCode) {
|
|
||||||
if (!(await confirmAction({
|
|
||||||
title: 'Reset client',
|
|
||||||
message: `Reset client "${clientCode}"? This clears all questionnaire answers, upload history, and scoring results. The client code and counselor assignment are kept.`,
|
|
||||||
confirmLabel: 'Reset',
|
|
||||||
variant: 'danger',
|
|
||||||
}))) return;
|
|
||||||
try {
|
|
||||||
await apiPost('clients.php', { action: 'reset', clientCode });
|
|
||||||
const row = clientsList.find(c => c.clientCode === clientCode);
|
|
||||||
if (row) {
|
|
||||||
row.hasResponseData = 0;
|
|
||||||
row.scoringProfiles = (row.scoringProfiles || []).map(p => ({
|
|
||||||
...p,
|
|
||||||
band: null,
|
|
||||||
calculatedBand: null,
|
|
||||||
coachBand: null,
|
|
||||||
effectiveBand: null,
|
|
||||||
pendingReview: false,
|
|
||||||
coachOverride: false,
|
|
||||||
coachReviewedAt: '',
|
|
||||||
coachReviewedByUsername: '',
|
|
||||||
weightedTotal: null,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
delete detailByClient[clientCode];
|
|
||||||
if (expandedClientCode === clientCode) {
|
|
||||||
expandedClientCode = null;
|
|
||||||
expandedQnKey = null;
|
|
||||||
expandedVersionKey = null;
|
|
||||||
}
|
|
||||||
showToast(`Client "${clientCode}" reset`, 'success');
|
|
||||||
renderClientTableBody();
|
|
||||||
} catch (e) {
|
|
||||||
showToast(e.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteClient(clientCode) {
|
async function deleteClient(clientCode) {
|
||||||
if (!(await confirmAction({
|
if (!(await confirmAction({
|
||||||
title: 'Delete client',
|
title: 'Delete client',
|
||||||
|
|||||||
Reference in New Issue
Block a user