| |
|
|
| |
| 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'); |
| }); |
| }); |
|
|
| |
| document.getElementById('logoutBtn').addEventListener('click', async () => { |
| await fetch('/api/logout', { method: 'POST' }); |
| window.location.href = '/login'; |
| }); |
|
|
| |
| fetch('/api/me').then(r => r.json()).then(u => { |
| document.getElementById('userBadge').textContent = u.username; |
| }); |
|
|
| |
| let allUsers = []; |
| let allFiles = []; |
|
|
| |
| 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(''); |
| } |
|
|
| |
| 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(''); |
| } |
|
|
| |
| 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(); |
| } |
|
|
| |
| 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() { |
| |
| 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(); |
| } |
|
|
| |
| loadFiles(); |
| loadUsers(); |
| loadAssignments(); |
|
|