This commit is contained in:
@ -29,6 +29,12 @@
|
||||
Sessions
|
||||
</a>
|
||||
</li>
|
||||
<li data-nav-roles="admin supervisor">
|
||||
<a href="#/schedule">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M8 14h.01M12 14h.01M16 14h.01M8 18h.01M12 18h.01"/></svg>
|
||||
Schedule
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/questionnaires">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/></svg>
|
||||
|
||||
@ -14,6 +14,7 @@ import { devPage } from './pages/dev.js';
|
||||
import { insightsPage } from './pages/insights.js';
|
||||
import { coachesPage } from './pages/coaches.js';
|
||||
import { sessionsPage } from './pages/sessions.js';
|
||||
import { schedulePage } from './pages/schedule.js';
|
||||
import { openChangeOwnPasswordModal } from './password-modal.js';
|
||||
|
||||
// Auth state
|
||||
@ -136,6 +137,9 @@ function updateNav() {
|
||||
if (!active && href === '/sessions' && hash.startsWith('/sessions')) {
|
||||
active = true;
|
||||
}
|
||||
if (!active && href === '/schedule' && hash.startsWith('/schedule')) {
|
||||
active = true;
|
||||
}
|
||||
if (!active && href !== '/' && hash.startsWith(href)) {
|
||||
active = true;
|
||||
}
|
||||
@ -160,6 +164,7 @@ addRoute('/login', loginPage);
|
||||
addRoute('/', roleGuard(['admin', 'supervisor'], homeDashboardPage));
|
||||
addRoute('/questionnaires', authGuard(questionnairesPage));
|
||||
addRoute('/sessions', roleGuard(['admin', 'supervisor'], sessionsPage));
|
||||
addRoute('/schedule', roleGuard(['admin', 'supervisor'], schedulePage));
|
||||
addRoute('/questionnaire/new', authGuard(editorPage));
|
||||
addRoute('/questionnaire/:id/results', authGuard(resultsPage));
|
||||
addRoute('/questionnaire/:id', authGuard(editorPage));
|
||||
|
||||
@ -62,7 +62,7 @@ function renderExport() {
|
||||
<th>Version</th>
|
||||
<th>State</th>
|
||||
<th>Questions</th>
|
||||
<th>Completed</th>
|
||||
<th>Uploaded responses</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -133,7 +133,7 @@ function exportRowHTML(q) {
|
||||
<td>${esc(q.version || '—')}</td>
|
||||
<td><span class="badge badge-${(q.state || 'draft').toLowerCase()}">${esc(q.state || 'draft')}</span></td>
|
||||
<td>${q.questionCount || 0}</td>
|
||||
<td>${q.completedCount ?? 0}</td>
|
||||
<td>${q.submissionCount ?? 0}</td>
|
||||
<td class="export-actions-cell">
|
||||
<button class="btn btn-sm btn-primary export-btn" data-id="${esc(q.questionnaireID)}" data-name="${esc(q.name)}" data-version="${esc(q.version)}" data-all="0">
|
||||
Export current
|
||||
|
||||
108
website/js/pages/schedule.js
Normal file
108
website/js/pages/schedule.js
Normal file
@ -0,0 +1,108 @@
|
||||
import { apiGet } from '../api.js';
|
||||
import { pageHeaderHTML, showToast } from '../app.js';
|
||||
|
||||
export async function schedulePage() {
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = `
|
||||
${pageHeaderHTML('Schedule', '', {
|
||||
subtitle: 'Upcoming session appointments set by counselors in the app',
|
||||
})}
|
||||
<div id="scheduleContent"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const data = await apiGet('schedule');
|
||||
renderSchedule(data.entries || [], data.byDate || {});
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
document.getElementById('scheduleContent').innerHTML =
|
||||
`<p class="error-text">${esc(e.message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSchedule(entries, byDate) {
|
||||
const el = document.getElementById('scheduleContent');
|
||||
const todayStart = new Date();
|
||||
todayStart.setHours(0, 0, 0, 0);
|
||||
const futureEntries = entries.filter(e => parseDdMmYyyy(e.scheduledDate) > todayStart.getTime());
|
||||
const futureByDate = {};
|
||||
futureEntries.forEach(e => {
|
||||
(futureByDate[e.scheduledDate] ||= []).push(e);
|
||||
});
|
||||
|
||||
if (!futureEntries.length) {
|
||||
el.innerHTML = `
|
||||
<div class="card">
|
||||
<p class="scoring-empty-hint" style="margin:0">No upcoming session appointments.</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const dates = Object.keys(futureByDate).sort((a, b) => parseDdMmYyyy(a) - parseDdMmYyyy(b));
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<p class="data-toolbar-hint" style="margin:0">
|
||||
${futureEntries.length} scheduled client${futureEntries.length === 1 ? '' : 's'} across ${dates.length} date${dates.length === 1 ? '' : 's'}.
|
||||
Counselors set dates in the mobile app.
|
||||
</p>
|
||||
</div>
|
||||
${dates.map(date => dateGroupHTML(date, futureByDate[date] || [])).join('')}
|
||||
`;
|
||||
}
|
||||
|
||||
function dateGroupHTML(scheduledDate, rows) {
|
||||
const sorted = [...rows].sort((a, b) =>
|
||||
String(a.coachUsername || '').localeCompare(String(b.coachUsername || ''))
|
||||
|| String(a.clientCode).localeCompare(String(b.clientCode)),
|
||||
);
|
||||
return `
|
||||
<div class="card scoring-profiles-card" style="margin-bottom:16px">
|
||||
<div class="scoring-profile-header">
|
||||
<div>
|
||||
<h3 class="scoring-section-title">${esc(scheduledDate)}</h3>
|
||||
<p class="scoring-section-desc">
|
||||
${sorted.length} client${sorted.length === 1 ? '' : 's'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client</th>
|
||||
<th>Cycle</th>
|
||||
<th>Session</th>
|
||||
<th>Counselor</th>
|
||||
<th>Supervisor</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${sorted.map(row => `
|
||||
<tr>
|
||||
<td>${esc(row.clientCode)}</td>
|
||||
<td>${esc(String(row.programCycleNumber))}</td>
|
||||
<td>${esc(String(row.programSessionNumber))}</td>
|
||||
<td>${esc(row.coachUsername || row.coachID || '—')}</td>
|
||||
<td>${esc(row.supervisorUsername || '—')}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function parseDdMmYyyy(date) {
|
||||
const m = /^(\d{2})-(\d{2})-(\d{4})$/.exec(String(date));
|
||||
if (!m) return 0;
|
||||
return new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1])).getTime();
|
||||
}
|
||||
|
||||
function esc(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
Reference in New Issue
Block a user