new system prototype
This commit is contained in:
150
website/js/pages/dashboard.js
Normal file
150
website/js/pages/dashboard.js
Normal file
@ -0,0 +1,150 @@
|
||||
import { apiGet, apiPut, apiDelete } from '../api.js';
|
||||
import { canEdit, showToast, getRole } from '../app.js';
|
||||
import { navigate } from '../router.js';
|
||||
|
||||
export async function dashboardPage() {
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1>Questionnaires</h1>
|
||||
<div class="actions" id="headerActions"></div>
|
||||
</div>
|
||||
<div id="qGrid" class="q-grid"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
if (canEdit()) {
|
||||
document.getElementById('headerActions').innerHTML = `
|
||||
<a href="#/questionnaire/new" class="btn btn-primary">+ New Questionnaire</a>
|
||||
`;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await apiGet('questionnaires.php');
|
||||
renderGrid(data.questionnaires || []);
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
document.getElementById('qGrid').innerHTML = `<p class="error-text">${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderGrid(questionnaires) {
|
||||
const grid = document.getElementById('qGrid');
|
||||
|
||||
if (!questionnaires.length) {
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state" style="grid-column:1/-1">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 12h6m-3-3v6m-7 4h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
||||
<h3>No questionnaires yet</h3>
|
||||
<p>Create your first questionnaire to get started.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = questionnaires.map(q => {
|
||||
const stateClass = `badge-${(q.state || 'draft').toLowerCase().replace(/\s+/g, '_')}`;
|
||||
const showPts = parseInt(q.showPoints || 0, 10);
|
||||
return `
|
||||
<div class="q-card" data-id="${q.questionnaireID}" draggable="${canEdit()}">
|
||||
<div class="q-card-header">
|
||||
<div class="q-card-title">
|
||||
${canEdit() ? '<span class="drag-handle" title="Drag to reorder" style="margin-right:6px;cursor:grab;color:var(--text-secondary)">☰</span>' : ''}
|
||||
${esc(q.name)}
|
||||
</div>
|
||||
<span class="badge ${stateClass}">${esc(q.state || 'draft')}</span>
|
||||
</div>
|
||||
<div class="q-card-meta">
|
||||
<span>v${esc(q.version || '—')}</span>
|
||||
<span>${q.questionCount || 0} question${q.questionCount == 1 ? '' : 's'}</span>
|
||||
<span>#${q.orderIndex ?? '—'}</span>
|
||||
${showPts ? '<span class="badge badge-active" style="font-size:.7rem;padding:1px 6px">pts</span>' : ''}
|
||||
</div>
|
||||
<div class="q-card-actions">
|
||||
<a href="#/questionnaire/${q.questionnaireID}" class="btn btn-sm">Edit</a>
|
||||
<a href="#/questionnaire/${q.questionnaireID}/results" class="btn btn-sm">Results</a>
|
||||
${getRole() === 'admin' ? `<button class="btn btn-sm btn-danger delete-q-btn" data-id="${q.questionnaireID}">Delete</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
grid.querySelectorAll('.q-card').forEach(card => {
|
||||
card.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.q-card-actions') || e.target.closest('.drag-handle')) return;
|
||||
navigate(`#/questionnaire/${card.dataset.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
grid.querySelectorAll('.delete-q-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
if (!confirm('Delete this questionnaire and all its data?')) return;
|
||||
try {
|
||||
await apiDelete('questionnaires.php', { questionnaireID: btn.dataset.id });
|
||||
showToast('Questionnaire deleted', 'success');
|
||||
btn.closest('.q-card').remove();
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (canEdit()) initGridDrag(questionnaires);
|
||||
}
|
||||
|
||||
function initGridDrag(questionnaires) {
|
||||
const grid = document.getElementById('qGrid');
|
||||
if (!grid) return;
|
||||
let dragItem = null;
|
||||
|
||||
grid.addEventListener('dragstart', (e) => {
|
||||
const card = e.target.closest('.q-card');
|
||||
if (!card || !e.target.closest('.drag-handle')) { e.preventDefault(); return; }
|
||||
dragItem = card;
|
||||
card.style.opacity = '0.5';
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
});
|
||||
|
||||
grid.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
if (!dragItem) return;
|
||||
const cards = [...grid.querySelectorAll('.q-card:not([style*="opacity"])')];
|
||||
const afterCard = cards.reduce((closest, child) => {
|
||||
const box = child.getBoundingClientRect();
|
||||
const offset = e.clientY - box.top - box.height / 2;
|
||||
if (offset < 0 && offset > closest.offset) return { offset, element: child };
|
||||
return closest;
|
||||
}, { offset: Number.NEGATIVE_INFINITY }).element;
|
||||
|
||||
if (afterCard) grid.insertBefore(dragItem, afterCard);
|
||||
else grid.appendChild(dragItem);
|
||||
});
|
||||
|
||||
grid.addEventListener('dragend', async () => {
|
||||
if (!dragItem) return;
|
||||
dragItem.style.opacity = '';
|
||||
const newOrder = [...grid.querySelectorAll('.q-card')].map(el => el.dataset.id);
|
||||
dragItem = null;
|
||||
|
||||
for (let i = 0; i < newOrder.length; i++) {
|
||||
const id = newOrder[i];
|
||||
const q = questionnaires.find(q => q.questionnaireID === id);
|
||||
if (q && parseInt(q.orderIndex, 10) !== i) {
|
||||
try {
|
||||
await apiPut('questionnaires.php', { questionnaireID: id, orderIndex: i });
|
||||
q.orderIndex = i;
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
showToast('Order saved', 'success');
|
||||
});
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s ?? '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
Reference in New Issue
Block a user