File size: 7,570 Bytes
a73045d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | // ===== Admin Dashboard JS =====
// ββ Tab switching ββ
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
});
});
// ββ Logout ββ
document.getElementById('logoutBtn').addEventListener('click', async () => {
await fetch('/api/logout', { method: 'POST' });
window.location.href = '/login';
});
// ββ User info ββ
fetch('/api/me').then(r => r.json()).then(u => {
document.getElementById('userBadge').textContent = u.username;
});
// ββ State ββ
let allUsers = [];
let allFiles = [];
// ββ Files ββ
async function loadFiles() {
const res = await fetch('/api/files');
allFiles = await res.json();
const grid = document.getElementById('filesList');
if (allFiles.length === 0) {
grid.innerHTML = '<div class="empty-state"><p>No CSV files found in the data folder.</p></div>';
return;
}
grid.innerHTML = allFiles.map(f => {
return `
<div class="file-card" style="cursor:default">
<div class="file-card-header">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--accent);flex-shrink:0">
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
</svg>
<span class="file-card-name">${f.filename}</span>
</div>
<div class="file-card-stats">
<span>${f.total_samples} samples</span>
<span>${f.total_assignees} assignee(s)</span>
</div>
<div class="file-card-stats" style="margin-top:4px">
<span>${f.total_reviews} total reviews</span>
</div>
</div>`;
}).join('');
}
// ββ Users ββ
async function loadUsers() {
const res = await fetch('/api/users');
allUsers = await res.json();
const tbody = document.getElementById('usersBody');
tbody.innerHTML = allUsers.map(u => {
const roleClass = u.role === 'admin' ? 'role-admin' : 'role-annotator';
const created = u.created_at ? new Date(u.created_at).toLocaleDateString() : 'β';
return `<tr>
<td>${u.id}</td>
<td style="font-weight:600;color:var(--text-primary)">${u.username}</td>
<td><span class="role-badge ${roleClass}">${u.role}</span></td>
<td>${created}</td>
<td>${u.role !== 'admin' ? `<button class="danger-btn" onclick="deleteUser(${u.id})">Delete</button>` : 'β'}</td>
</tr>`;
}).join('');
}
// ββ Create User ββ
document.getElementById('addUserBtn').addEventListener('click', () => {
document.getElementById('createUserForm').style.display = 'block';
document.getElementById('newUsername').focus();
});
document.getElementById('cancelUserBtn').addEventListener('click', () => {
document.getElementById('createUserForm').style.display = 'none';
document.getElementById('userFormError').textContent = '';
});
document.getElementById('submitUserBtn').addEventListener('click', async () => {
const username = document.getElementById('newUsername').value.trim();
const password = document.getElementById('newPassword').value;
const role = document.getElementById('newRole').value;
const errEl = document.getElementById('userFormError');
errEl.textContent = '';
const res = await fetch('/api/users', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, role })
});
const data = await res.json();
if (!res.ok) { errEl.textContent = data.error; return; }
document.getElementById('createUserForm').style.display = 'none';
document.getElementById('newUsername').value = '';
document.getElementById('newPassword').value = '';
loadUsers();
loadAssignDropdowns();
});
async function deleteUser(id) {
if (!confirm('Delete this user?')) return;
await fetch(`/api/users/${id}`, { method: 'DELETE' });
loadUsers();
loadAssignments();
}
// ββ Assignments ββ
async function loadAssignments() {
const res = await fetch('/api/assignments');
const data = await res.json();
const tbody = document.getElementById('assignBody');
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--text-muted)">No assignments yet</td></tr>';
return;
}
tbody.innerHTML = data.map(a => {
const pct = a.progress;
const barColor = pct >= 100 ? '#34d399' : pct > 50 ? '#fbbf24' : '#60a5fa';
const date = a.assigned_at ? new Date(a.assigned_at).toLocaleDateString() : 'β';
return `<tr>
<td style="font-weight:600;color:var(--text-primary)">${a.username}</td>
<td>${a.filename}</td>
<td>
<div class="progress-cell">
<div class="progress-bar"><div class="progress-fill" style="width:${pct}%;background:${barColor}"></div></div>
<span class="progress-text" style="color:${barColor}">${a.reviewed_count}/${a.total_samples} (${pct}%)</span>
</div>
</td>
<td>${date}</td>
<td>
<button class="danger-btn" onclick="deleteAssignment(${a.id})">Remove</button>
</td>
</tr>`;
}).join('');
}
async function loadAssignDropdowns() {
// Populate user dropdown
const userSel = document.getElementById('assignUser');
const fileSel = document.getElementById('assignFile');
const usersRes = await fetch('/api/users');
const users = await usersRes.json();
const filesRes = await fetch('/api/files');
const files = await filesRes.json();
userSel.innerHTML = '<option value="">Select User...</option>' +
users.filter(u => u.role === 'annotator').map(u => `<option value="${u.id}">${u.username}</option>`).join('');
fileSel.innerHTML = '<option value="">Select File...</option>' +
files.map(f => `<option value="${f.id}">${f.filename} (${f.total_samples} samples)</option>`).join('');
}
document.getElementById('addAssignBtn').addEventListener('click', () => {
document.getElementById('createAssignForm').style.display = 'block';
loadAssignDropdowns();
});
document.getElementById('cancelAssignBtn').addEventListener('click', () => {
document.getElementById('createAssignForm').style.display = 'none';
document.getElementById('assignFormError').textContent = '';
});
document.getElementById('submitAssignBtn').addEventListener('click', async () => {
const user_id = document.getElementById('assignUser').value;
const file_id = document.getElementById('assignFile').value;
const errEl = document.getElementById('assignFormError');
errEl.textContent = '';
if (!user_id || !file_id) { errEl.textContent = 'Select both user and file'; return; }
const res = await fetch('/api/assignments', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: parseInt(user_id), file_id: parseInt(file_id) })
});
const data = await res.json();
if (!res.ok) { errEl.textContent = data.error; return; }
document.getElementById('createAssignForm').style.display = 'none';
loadAssignments();
loadFiles();
});
async function deleteAssignment(id) {
if (!confirm('Remove this assignment?')) return;
await fetch(`/api/assignments/${id}`, { method: 'DELETE' });
loadAssignments();
loadFiles();
}
// ββ Init ββ
loadFiles();
loadUsers();
loadAssignments();
|