275 lines
12 KiB
JavaScript
275 lines
12 KiB
JavaScript
import { apiGet, apiPost } from '../api.js';
|
|
import { showToast } from '../app.js';
|
|
import { isDevTestClientCode, isDevTestUsername, testDataRowClassForClient } from '../test-data.js';
|
|
|
|
let coaches = [];
|
|
let clients = [];
|
|
let selectedCoachID = null;
|
|
|
|
export async function assignmentsPage() {
|
|
selectedCoachID = null;
|
|
const app = document.getElementById('app');
|
|
|
|
app.innerHTML = `
|
|
<div class="page-header">
|
|
<h1>Assign Clients</h1>
|
|
<div class="actions"><a href="#/" class="btn">← Dashboard</a></div>
|
|
</div>
|
|
<div id="assignContent"><div class="spinner"></div></div>
|
|
`;
|
|
|
|
try {
|
|
const data = await apiGet('assignments.php');
|
|
coaches = data.coaches || [];
|
|
clients = data.clients || [];
|
|
renderAssignments();
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
document.getElementById('assignContent').innerHTML =
|
|
`<p class="error-text" style="padding:16px">${esc(e.message)}</p>`;
|
|
}
|
|
}
|
|
|
|
function renderAssignments() {
|
|
const container = document.getElementById('assignContent');
|
|
|
|
if (!coaches.length) {
|
|
container.innerHTML = `
|
|
<div class="card empty-state">
|
|
<h3>No coaches found</h3>
|
|
<p>Create coaches first before assigning clients.</p>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
// Group clients by coachID for the filter badge counts
|
|
const clientsByCoach = {};
|
|
const unassigned = [];
|
|
clients.forEach(c => {
|
|
if (c.coachID) {
|
|
(clientsByCoach[c.coachID] = clientsByCoach[c.coachID] || []).push(c);
|
|
} else {
|
|
unassigned.push(c);
|
|
}
|
|
});
|
|
|
|
container.innerHTML = `
|
|
<div class="assign-layout">
|
|
|
|
<!-- Left: Coach list -->
|
|
<div class="assign-coaches card">
|
|
<h3 style="margin-bottom:12px">Coaches</h3>
|
|
<ul class="coach-list" id="coachList">
|
|
${coaches.map(c => {
|
|
const count = (clientsByCoach[c.coachID] || []).length;
|
|
const coachTest = isDevTestUsername(c.username) ? ' row-test-data' : '';
|
|
return `
|
|
<li class="coach-list-item${coachTest}" data-id="${c.coachID}">
|
|
<div class="coach-name">${esc(c.username)}</div>
|
|
${c.supervisorUsername
|
|
? `<div class="coach-supervisor">${esc(c.supervisorUsername)}</div>`
|
|
: ''}
|
|
<span class="coach-client-count">${count} client${count === 1 ? '' : 's'}</span>
|
|
</li>`;
|
|
}).join('')}
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Right: Client list + assign panel -->
|
|
<div class="assign-right">
|
|
<div class="card assign-clients-card">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px">
|
|
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
|
<h3>Clients</h3>
|
|
<div class="filter-bar" style="margin:0">
|
|
<select id="filterByCoach">
|
|
<option value="">All coaches</option>
|
|
<option value="__unassigned__">Unassigned</option>
|
|
${coaches.map(c =>
|
|
`<option value="${c.coachID}">${esc(c.username)}</option>`
|
|
).join('')}
|
|
</select>
|
|
<input type="text" id="clientSearch" placeholder="Search client code..." style="width:160px">
|
|
<select id="clientTestFilter">
|
|
<option value="">All clients</option>
|
|
<option value="test">Test only</option>
|
|
<option value="hide-test">Hide test</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;gap:6px">
|
|
<button class="btn btn-sm" id="selectAllBtn">Select all</button>
|
|
<button class="btn btn-sm" id="clearSelBtn">Clear</button>
|
|
</div>
|
|
</div>
|
|
<div class="table-wrapper" style="max-height:420px">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:36px"><input type="checkbox" id="selectAllCb" title="Toggle all visible"></th>
|
|
<th>Client Code</th>
|
|
<th>Current Coach</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="clientTableBody"></tbody>
|
|
</table>
|
|
</div>
|
|
<div id="selectionInfo" class="selection-info" style="display:none"></div>
|
|
</div>
|
|
|
|
<!-- Assign action card -->
|
|
<div class="card assign-action-card" id="assignActionCard" style="display:none">
|
|
<h4>Assign selected clients to:</h4>
|
|
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:10px">
|
|
<select id="targetCoachSelect" style="flex:1;min-width:180px;padding:8px 12px;border:1px solid var(--border);border-radius:6px">
|
|
<option value="">— select coach —</option>
|
|
${coaches.map(c =>
|
|
`<option value="${c.coachID}">${esc(c.username)}${c.supervisorUsername ? ` (${esc(c.supervisorUsername)})` : ''}</option>`
|
|
).join('')}
|
|
</select>
|
|
<button class="btn btn-primary" id="assignBtn">Assign</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Wire coach list click → filter clients
|
|
document.querySelectorAll('.coach-list-item').forEach(li => {
|
|
li.addEventListener('click', () => {
|
|
document.querySelectorAll('.coach-list-item').forEach(x => x.classList.remove('active'));
|
|
li.classList.add('active');
|
|
selectedCoachID = li.dataset.id;
|
|
document.getElementById('filterByCoach').value = selectedCoachID;
|
|
renderClientRows();
|
|
});
|
|
});
|
|
|
|
// Wire filters
|
|
document.getElementById('filterByCoach').addEventListener('change', renderClientRows);
|
|
document.getElementById('clientSearch').addEventListener('input', renderClientRows);
|
|
document.getElementById('clientTestFilter').addEventListener('change', renderClientRows);
|
|
|
|
// Wire select all / clear
|
|
document.getElementById('selectAllBtn').addEventListener('click', () => {
|
|
document.querySelectorAll('#clientTableBody .client-cb').forEach(cb => cb.checked = true);
|
|
updateSelectionInfo();
|
|
});
|
|
document.getElementById('clearSelBtn').addEventListener('click', () => {
|
|
document.querySelectorAll('#clientTableBody .client-cb').forEach(cb => cb.checked = false);
|
|
updateSelectionInfo();
|
|
});
|
|
document.getElementById('selectAllCb').addEventListener('change', (e) => {
|
|
document.querySelectorAll('#clientTableBody .client-cb').forEach(cb => cb.checked = e.target.checked);
|
|
updateSelectionInfo();
|
|
});
|
|
|
|
// Wire assign button
|
|
document.getElementById('assignBtn').addEventListener('click', doAssign);
|
|
|
|
renderClientRows();
|
|
}
|
|
|
|
function renderClientRows() {
|
|
const body = document.getElementById('clientTableBody');
|
|
const coachFilter = document.getElementById('filterByCoach').value;
|
|
const search = document.getElementById('clientSearch').value.trim().toLowerCase();
|
|
const testMode = document.getElementById('clientTestFilter')?.value || '';
|
|
|
|
let visible = clients;
|
|
if (coachFilter === '__unassigned__') {
|
|
visible = clients.filter(c => !c.coachID);
|
|
} else if (coachFilter) {
|
|
visible = clients.filter(c => c.coachID === coachFilter);
|
|
}
|
|
if (search) {
|
|
visible = visible.filter(c => c.clientCode.toLowerCase().includes(search));
|
|
}
|
|
if (testMode === 'test') {
|
|
visible = visible.filter(c => isDevTestClientCode(c.clientCode));
|
|
} else if (testMode === 'hide-test') {
|
|
visible = visible.filter(c => !isDevTestClientCode(c.clientCode));
|
|
}
|
|
|
|
if (!visible.length) {
|
|
body.innerHTML = `<tr><td colspan="3" style="text-align:center;color:var(--text-secondary);padding:24px">No clients match the current filter</td></tr>`;
|
|
updateSelectionInfo();
|
|
return;
|
|
}
|
|
|
|
body.innerHTML = visible.map(c => `
|
|
<tr class="${testDataRowClassForClient(c.clientCode).trim()}">
|
|
<td><input type="checkbox" class="client-cb" data-code="${esc(c.clientCode)}" value="${esc(c.clientCode)}"></td>
|
|
<td><strong>${esc(c.clientCode)}</strong></td>
|
|
<td>${c.coachUsername ? esc(c.coachUsername) : '<span style="color:var(--text-secondary);font-style:italic">Unassigned</span>'}</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
body.querySelectorAll('.client-cb').forEach(cb => {
|
|
cb.addEventListener('change', updateSelectionInfo);
|
|
});
|
|
|
|
updateSelectionInfo();
|
|
}
|
|
|
|
function getSelectedCodes() {
|
|
return [...document.querySelectorAll('#clientTableBody .client-cb:checked')]
|
|
.map(cb => cb.value);
|
|
}
|
|
|
|
function updateSelectionInfo() {
|
|
const selected = getSelectedCodes();
|
|
const info = document.getElementById('selectionInfo');
|
|
const card = document.getElementById('assignActionCard');
|
|
|
|
if (!selected.length) {
|
|
info.style.display = 'none';
|
|
card.style.display = 'none';
|
|
return;
|
|
}
|
|
info.style.display = '';
|
|
info.innerHTML = `<strong>${selected.length}</strong> client${selected.length === 1 ? '' : 's'} selected`;
|
|
card.style.display = '';
|
|
}
|
|
|
|
async function doAssign() {
|
|
const selected = getSelectedCodes();
|
|
const coachID = document.getElementById('targetCoachSelect').value;
|
|
|
|
if (!selected.length) { showToast('Select at least one client', 'error'); return; }
|
|
if (!coachID) { showToast('Select a target coach', 'error'); return; }
|
|
|
|
const coachName = coaches.find(c => c.coachID === coachID)?.username || coachID;
|
|
if (!confirm(`Assign ${selected.length} client(s) to ${coachName}?`)) return;
|
|
|
|
const btn = document.getElementById('assignBtn');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Assigning...';
|
|
|
|
try {
|
|
const data = await apiPost('assignments.php', { clientCodes: selected, coachID });
|
|
if (data.success) {
|
|
// Update local data
|
|
selected.forEach(code => {
|
|
const c = clients.find(x => x.clientCode === code);
|
|
if (c) {
|
|
c.coachID = coachID;
|
|
c.coachUsername = coachName;
|
|
}
|
|
});
|
|
showToast(`${data.assigned} client(s) assigned to ${coachName}`, 'success');
|
|
renderAssignments();
|
|
}
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
btn.disabled = false;
|
|
btn.textContent = 'Assign';
|
|
}
|
|
}
|
|
|
|
function esc(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s ?? '';
|
|
return d.innerHTML;
|
|
}
|