193 lines
7.0 KiB
JavaScript
193 lines
7.0 KiB
JavaScript
import { addRoute, startRouter, navigate } from './router.js';
|
|
import { ensureValidSession, invalidateSessionCheck } from './api.js';
|
|
import { loginPage } from './pages/login.js';
|
|
import { homeDashboardPage } from './pages/home.js';
|
|
import { questionnairesPage } from './pages/questionnaires.js';
|
|
import { editorPage } from './pages/editor.js';
|
|
import { resultsPage } from './pages/results.js';
|
|
import { exportPage } from './pages/export.js';
|
|
import { usersPage } from './pages/users.js';
|
|
import { assignmentsPage } from './pages/assignments.js';
|
|
import { clientsPage } from './pages/clients.js';
|
|
import { translationsPage } from './pages/translations.js';
|
|
import { devPage } from './pages/dev.js';
|
|
import { insightsPage } from './pages/insights.js';
|
|
import { coachesPage } from './pages/coaches.js';
|
|
import { openChangeOwnPasswordModal } from './password-modal.js';
|
|
|
|
// Auth state
|
|
export function isLoggedIn() {
|
|
return !!localStorage.getItem('qdb_token');
|
|
}
|
|
export function getRole() {
|
|
return localStorage.getItem('qdb_role') || '';
|
|
}
|
|
export function getUser() {
|
|
return localStorage.getItem('qdb_user') || '';
|
|
}
|
|
const WEBSITE_ROLES = ['admin', 'supervisor'];
|
|
|
|
export function canAccessWebsite() {
|
|
return WEBSITE_ROLES.includes(getRole());
|
|
}
|
|
|
|
export function canEdit() {
|
|
return canAccessWebsite();
|
|
}
|
|
|
|
/** User-facing label for the coach role (internal key remains `coach`). */
|
|
export function formatRoleLabel(role) {
|
|
if (role === 'coach') return 'Counselor';
|
|
if (!role) return '';
|
|
return role.charAt(0).toUpperCase() + role.slice(1);
|
|
}
|
|
|
|
function clearSession() {
|
|
invalidateSessionCheck();
|
|
localStorage.removeItem('qdb_token');
|
|
localStorage.removeItem('qdb_role');
|
|
localStorage.removeItem('qdb_user');
|
|
}
|
|
|
|
function logout() {
|
|
clearSession();
|
|
navigate('#/login');
|
|
}
|
|
|
|
/** Link to home dashboard (#/). */
|
|
export const HOME_HREF = '#/';
|
|
|
|
const HOME_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1h-5v-7H9v7H4a1 1 0 01-1-1V9.5z"/></svg>`;
|
|
|
|
/** House icon button — returns to dashboard. */
|
|
export function homeNavButton(title = 'Overview') {
|
|
return `<a href="${HOME_HREF}" class="btn btn-home" title="${title}" aria-label="${title}">${HOME_ICON_SVG}</a>`;
|
|
}
|
|
|
|
/**
|
|
* Standard page header with home button.
|
|
* @param {string} title HTML-safe title text
|
|
* @param {string} [actionsHTML] optional buttons inside .actions
|
|
* @param {{ subtitle?: string }} [opts]
|
|
*/
|
|
export function pageHeaderHTML(title, actionsHTML = '', opts = {}) {
|
|
const subtitle = opts.subtitle
|
|
? `<p class="page-header-sub">${opts.subtitle}</p>`
|
|
: '';
|
|
return `
|
|
<div class="page-header">
|
|
<div class="page-header-start">
|
|
${homeNavButton()}
|
|
<div class="page-header-titles">
|
|
<h1>${title}</h1>
|
|
${subtitle}
|
|
</div>
|
|
</div>
|
|
${actionsHTML ? `<div class="actions">${actionsHTML}</div>` : ''}
|
|
</div>`;
|
|
}
|
|
|
|
export function showToast(message, type = 'info') {
|
|
const container = document.getElementById('toastContainer');
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast toast-${type}`;
|
|
toast.textContent = message;
|
|
container.appendChild(toast);
|
|
requestAnimationFrame(() => toast.classList.add('show'));
|
|
setTimeout(() => {
|
|
toast.classList.remove('show');
|
|
setTimeout(() => toast.remove(), 300);
|
|
}, 3000);
|
|
}
|
|
|
|
function authGuard(handler) {
|
|
return async (params) => {
|
|
if (!(await ensureValidSession())) return;
|
|
return handler(params);
|
|
};
|
|
}
|
|
|
|
function updateNav() {
|
|
const nav = document.getElementById('mainNav');
|
|
const userInfo = document.getElementById('userInfo');
|
|
document.body.classList.toggle('logged-in', isLoggedIn());
|
|
const hash = window.location.hash.slice(1) || '/';
|
|
document.body.classList.toggle('login-active', hash === '/login' || hash.startsWith('/login?'));
|
|
if (isLoggedIn()) {
|
|
nav.style.display = '';
|
|
const role = getRole();
|
|
const roleLabel = formatRoleLabel(role);
|
|
userInfo.textContent = `${getUser()} (${roleLabel})`;
|
|
|
|
// Show/hide role-gated nav items
|
|
document.querySelectorAll('[data-nav-roles]').forEach(li => {
|
|
const allowed = li.dataset.navRoles.split(' ');
|
|
li.style.display = allowed.includes(role) ? '' : 'none';
|
|
});
|
|
|
|
// Highlight active nav link
|
|
document.querySelectorAll('.sidebar-nav a').forEach(a => {
|
|
const href = a.getAttribute('href').slice(1);
|
|
let active = hash === href;
|
|
if (!active && href === '/questionnaires' && hash.startsWith('/questionnaire')) {
|
|
active = true;
|
|
}
|
|
if (!active && href !== '/' && hash.startsWith(href)) {
|
|
active = true;
|
|
}
|
|
a.classList.toggle('active', active);
|
|
});
|
|
} else {
|
|
nav.style.display = 'none';
|
|
userInfo.textContent = '';
|
|
}
|
|
}
|
|
|
|
function roleGuard(roles, handler) {
|
|
return async (params) => {
|
|
if (!(await ensureValidSession())) return;
|
|
if (!roles.includes(getRole())) { navigate('#/'); return; }
|
|
return handler(params);
|
|
};
|
|
}
|
|
|
|
// Routes
|
|
addRoute('/login', loginPage);
|
|
addRoute('/', roleGuard(['admin', 'supervisor'], homeDashboardPage));
|
|
addRoute('/questionnaires', authGuard(questionnairesPage));
|
|
addRoute('/questionnaire/new', authGuard(editorPage));
|
|
addRoute('/questionnaire/:id/results', authGuard(resultsPage));
|
|
addRoute('/questionnaire/:id', authGuard(editorPage));
|
|
addRoute('/export', authGuard(exportPage));
|
|
addRoute('/users', roleGuard(['admin', 'supervisor'], usersPage));
|
|
addRoute('/assignments', roleGuard(['admin', 'supervisor'], assignmentsPage));
|
|
addRoute('/clients', roleGuard(['admin', 'supervisor'], clientsPage));
|
|
addRoute('/translations', roleGuard(['admin', 'supervisor'], translationsPage));
|
|
addRoute('/insights', roleGuard(['admin', 'supervisor'], insightsPage));
|
|
addRoute('/coaches', roleGuard(['admin', 'supervisor'], coachesPage));
|
|
addRoute('/dev', roleGuard(['admin'], devPage));
|
|
|
|
// Nav link handling & logout
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const bindLogout = (el) => {
|
|
if (!el) return;
|
|
el.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
logout();
|
|
});
|
|
};
|
|
bindLogout(document.getElementById('logoutBtn'));
|
|
bindLogout(document.getElementById('mobileLogoutBtn'));
|
|
|
|
const openOwnPassword = async () => {
|
|
if (!(await ensureValidSession())) return;
|
|
openChangeOwnPasswordModal();
|
|
};
|
|
document.getElementById('changePasswordBtn')?.addEventListener('click', openOwnPassword);
|
|
document.getElementById('mobileChangePasswordBtn')?.addEventListener('click', openOwnPassword);
|
|
|
|
window.addEventListener('hashchange', updateNav);
|
|
updateNav();
|
|
startRouter();
|
|
});
|