enhanced client results and export functionality with coach and supervisor details, added date filters for results

This commit is contained in:
2026-05-20 11:01:15 +02:00
parent 2a11dfd0d1
commit 03291862f7
7 changed files with 163 additions and 10 deletions

View File

@ -7,14 +7,20 @@ let allClients = [];
let questionsDef = [];
let questionnaireMeta = null;
let filterCoach = '';
let filterSupervisor = '';
let filterStatus = '';
let filterDateFrom = '';
let filterDateTo = '';
export async function resultsPage(params) {
const app = document.getElementById('app');
sortCol = null;
sortDir = 'asc';
filterCoach = '';
filterSupervisor = '';
filterStatus = '';
filterDateFrom = '';
filterDateTo = '';
app.innerHTML = `
<div class="page-header">
@ -44,21 +50,35 @@ function renderResults() {
const container = document.getElementById('resultsContent');
// Collect unique coaches and statuses for filters
const coaches = [...new Set(allClients.map(c => c.coachID))].sort();
const coachOptions = getCoachFilterOptions();
const supervisorOptions = getSupervisorFilterOptions();
const statuses = [...new Set(allClients.map(c => c.status))].filter(Boolean).sort();
container.innerHTML = `
<div class="card" style="margin-bottom:16px">
<div class="filter-bar">
<label style="font-size:.85rem;font-weight:500">Filter:</label>
<select id="filterSupervisor">
<option value="">All supervisors</option>
${supervisorOptions.map(name => `<option value="${esc(name)}" ${filterSupervisor === name ? 'selected' : ''}>${esc(name)}</option>`).join('')}
</select>
<select id="filterCoach">
<option value="">All coaches</option>
${coaches.map(c => `<option value="${esc(c)}" ${filterCoach === c ? 'selected' : ''}>${esc(c)}</option>`).join('')}
${coachOptions.map(([id, label]) => `<option value="${esc(id)}" ${filterCoach === id ? 'selected' : ''}>${esc(label)}</option>`).join('')}
</select>
<select id="filterStatus">
<option value="">All statuses</option>
${statuses.map(s => `<option value="${esc(s)}" ${filterStatus === s ? 'selected' : ''}>${esc(s)}</option>`).join('')}
</select>
<label style="font-size:.85rem;display:flex;align-items:center;gap:4px">
Completed from
<input type="date" id="filterDateFrom" value="${esc(filterDateFrom)}">
</label>
<label style="font-size:.85rem;display:flex;align-items:center;gap:4px">
to
<input type="date" id="filterDateTo" value="${esc(filterDateTo)}">
</label>
<button type="button" class="btn btn-sm" id="clearFiltersBtn">Clear filters</button>
<span style="margin-left:auto;font-size:.85rem;color:var(--text-secondary)" id="resultCount"></span>
<a id="exportCsvLink" class="btn btn-sm" href="#">Export CSV</a>
</div>
@ -73,6 +93,10 @@ function renderResults() {
</div>
`;
document.getElementById('filterSupervisor').addEventListener('change', (e) => {
filterSupervisor = e.target.value;
renderTableRows();
});
document.getElementById('filterCoach').addEventListener('change', (e) => {
filterCoach = e.target.value;
renderTableRows();
@ -81,6 +105,22 @@ function renderResults() {
filterStatus = e.target.value;
renderTableRows();
});
document.getElementById('filterDateFrom').addEventListener('change', (e) => {
filterDateFrom = e.target.value;
renderTableRows();
});
document.getElementById('filterDateTo').addEventListener('change', (e) => {
filterDateTo = e.target.value;
renderTableRows();
});
document.getElementById('clearFiltersBtn').addEventListener('click', () => {
filterCoach = '';
filterSupervisor = '';
filterStatus = '';
filterDateFrom = '';
filterDateTo = '';
renderResults();
});
document.getElementById('exportCsvLink').addEventListener('click', (e) => {
e.preventDefault();
exportCSV();
@ -94,7 +134,8 @@ function renderTableHead() {
const head = document.getElementById('resultsHead');
const fixedCols = [
{ key: 'clientCode', label: 'Client' },
{ key: 'coachID', label: 'Coach' },
{ key: 'coachUsername', label: 'Coach' },
{ key: 'supervisorUsername', label: 'Supervisor' },
{ key: 'status', label: 'Status' },
{ key: 'sumPoints', label: 'Score' },
{ key: 'completedAt', label: 'Completed' },
@ -123,13 +164,58 @@ function renderTableHead() {
});
}
function getCoachFilterOptions() {
const map = new Map();
allClients.forEach(c => {
if (!c.coachID || map.has(c.coachID)) return;
map.set(c.coachID, c.coachUsername || c.coachID);
});
return [...map.entries()].sort((a, b) => a[1].localeCompare(b[1]));
}
function getSupervisorFilterOptions() {
return [...new Set(allClients.map(c => c.supervisorUsername).filter(Boolean))].sort((a, b) =>
a.localeCompare(b)
);
}
function coachDisplayName(c) {
return c.coachUsername || c.coachID || '—';
}
function supervisorDisplayName(c) {
return c.supervisorUsername || '—';
}
function getFilteredClients() {
let clients = allClients;
if (filterSupervisor) clients = clients.filter(c => c.supervisorUsername === filterSupervisor);
if (filterCoach) clients = clients.filter(c => c.coachID === filterCoach);
if (filterStatus) clients = clients.filter(c => c.status === filterStatus);
if (filterDateFrom || filterDateTo) {
const fromTs = filterDateFrom ? dateInputToUnixStart(filterDateFrom) : null;
const toTs = filterDateTo ? dateInputToUnixEnd(filterDateTo) : null;
clients = clients.filter(c => {
const ts = c.completedAt;
if (!ts) return false;
if (fromTs !== null && ts < fromTs) return false;
if (toTs !== null && ts > toTs) return false;
return true;
});
}
return clients;
}
function dateInputToUnixStart(yyyyMmDd) {
const [y, m, d] = yyyyMmDd.split('-').map(Number);
return Math.floor(new Date(y, m - 1, d).getTime() / 1000);
}
function dateInputToUnixEnd(yyyyMmDd) {
const [y, m, d] = yyyyMmDd.split('-').map(Number);
return Math.floor(new Date(y, m - 1, d, 23, 59, 59, 999).getTime() / 1000);
}
function renderTableRows() {
const body = document.getElementById('resultsBody');
let clients = getFilteredClients();
@ -181,7 +267,8 @@ function renderTableRows() {
return `<tr>
<td>${esc(c.clientCode)}</td>
<td>${esc(c.coachID)}</td>
<td>${esc(coachDisplayName(c))}</td>
<td>${esc(supervisorDisplayName(c))}</td>
<td>${statusBadge}</td>
<td>${c.sumPoints !== null ? c.sumPoints : '—'}</td>
<td>${completedDate}</td>
@ -192,7 +279,8 @@ function renderTableRows() {
function getCellValue(client, key) {
if (key === 'clientCode') return client.clientCode;
if (key === 'coachID') return client.coachID;
if (key === 'coachUsername') return coachDisplayName(client);
if (key === 'supervisorUsername') return supervisorDisplayName(client);
if (key === 'status') return client.status;
if (key === 'sumPoints') return client.sumPoints;
if (key === 'completedAt') return client.completedAt;
@ -216,12 +304,12 @@ function exportCSV() {
});
});
const headers = ['clientCode', 'coachID', 'status', 'sumPoints', 'completedAt',
const headers = ['clientCode', 'coach', 'supervisor', 'status', 'sumPoints', 'completedAt',
...questionsDef.map(q => q.defaultText)];
const rows = clients.map(c => {
const base = [
c.clientCode, c.coachID, c.status, c.sumPoints ?? '',
c.clientCode, coachDisplayName(c), c.supervisorUsername || '', c.status, c.sumPoints ?? '',
c.completedAt ? new Date(c.completedAt * 1000).toISOString() : ''
];
const answerCols = questionsDef.map(q => {

View File

@ -19,6 +19,7 @@ export async function usersPage() {
<div class="page-header">
<h1>Users</h1>
<div class="actions">
<button class="btn" id="downloadUsersBtn" ${usersList.length ? '' : 'disabled'}>Download CSV</button>
<button class="btn btn-primary" id="showCreateFormBtn">+ Create User</button>
</div>
</div>
@ -34,6 +35,7 @@ export async function usersPage() {
hideCreateForm();
}
});
document.getElementById('downloadUsersBtn').addEventListener('click', downloadUsersCSV);
await loadUsers();
}
@ -45,6 +47,8 @@ async function loadUsers() {
supervisorsList = data.supervisors || [];
callerRole = data.callerRole || callerRole;
renderUsers();
const dlBtn = document.getElementById('downloadUsersBtn');
if (dlBtn) dlBtn.disabled = !usersList.length;
} catch (e) {
showToast(e.message, 'error');
document.getElementById('usersContent').innerHTML =
@ -319,6 +323,45 @@ function showError(el, msg) {
el.style.display = '';
}
function downloadUsersCSV() {
if (!usersList.length) {
showToast('No users to export', 'error');
return;
}
const headers = ['username', 'role', 'location', 'supervisor', 'mustChangePassword', 'createdAt', 'userID'];
const rows = usersList.map(u => [
u.username,
u.role,
u.role === 'coach' ? '' : (u.location || ''),
u.role === 'coach' ? (u.supervisorUsername || u.supervisorID || '') : '',
u.mustChangePassword == 1 ? 'yes' : 'no',
u.createdAt ? new Date(parseInt(u.createdAt, 10) * 1000).toISOString() : '',
u.userID,
]);
let csv = '\uFEFF';
csv += headers.map(csvEsc).join(',') + '\n';
rows.forEach(r => { csv += r.map(csvEsc).join(',') + '\n'; });
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `users_${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
showToast('Users exported', 'success');
}
function csvEsc(val) {
const s = String(val ?? '');
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
return '"' + s.replace(/"/g, '""') + '"';
}
return s;
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s ?? '';