Download admin.html from SammonsG/tech-support-queue-manager-lpvnm: direct link, hf CLI and curl.
- Browser
- Download file 15.1 kB
-
https://huggingface.co/spaces/SammonsG/tech-support-queue-manager-lpvnm/resolve/main/admin.html
- Command line
-
hf download hf://spaces/SammonsG/tech-support-queue-manager-lpvnm/admin.html
-
curl -L -o admin.html https://huggingface.co/spaces/SammonsG/tech-support-queue-manager-lpvnm/resolve/main/admin.html
15.1 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Tech Support Queue - Admin</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| body { font-family: 'Inter', sans-serif; } | |
| .queue-item { animation: slideIn 0.3s ease-out; } | |
| @keyframes slideIn { | |
| from { opacity: 0; transform: translateX(-10px); } | |
| to { opacity: 1; transform: translateX(0); } | |
| } | |
| .status-badge { transition: all 0.2s; } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 min-h-screen"> | |
| <!-- Header --> | |
| <header class="bg-white shadow-sm border-b border-gray-200 sticky top-0 z-10"> | |
| <div class="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between"> | |
| <div class="flex items-center gap-3"> | |
| <div class="bg-indigo-600 p-2 rounded-lg"> | |
| <i data-lucide="list" class="w-6 h-6 text-white"></i> | |
| </div> | |
| <div> | |
| <h1 class="text-xl font-bold text-gray-800">Support Queue</h1> | |
| <p class="text-sm text-gray-500">Real-time check-in monitoring</p> | |
| </div> | |
| </div> | |
| <div class="text-right"> | |
| <p class="text-2xl font-mono font-bold text-gray-800" id="clock">--:--:--</p> | |
| <p class="text-xs text-gray-500" id="date">Loading...</p> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="max-w-6xl mx-auto px-4 py-6"> | |
| <!-- Stats Bar --> | |
| <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6"> | |
| <div class="bg-white p-4 rounded-xl shadow-sm border border-gray-200"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm text-gray-500 mb-1">Waiting</p> | |
| <p class="text-2xl font-bold text-orange-600" id="statWaiting">0</p> | |
| </div> | |
| <div class="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center"> | |
| <i data-lucide="clock" class="w-5 h-5 text-orange-600"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white p-4 rounded-xl shadow-sm border border-gray-200"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm text-gray-500 mb-1">Serving</p> | |
| <p class="text-2xl font-bold text-blue-600" id="statServing">0</p> | |
| </div> | |
| <div class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center"> | |
| <i data-lucide="loader" class="w-5 h-5 text-blue-600"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white p-4 rounded-xl shadow-sm border border-gray-200"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm text-gray-500 mb-1">Completed</p> | |
| <p class="text-2xl font-bold text-green-600" id="statCompleted">0</p> | |
| </div> | |
| <div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center"> | |
| <i data-lucide="check-circle" class="w-5 h-5 text-green-600"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white p-4 rounded-xl shadow-sm border border-gray-200"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm text-gray-500 mb-1">Total Today</p> | |
| <p class="text-2xl font-bold text-indigo-600" id="statTotal">0</p> | |
| </div> | |
| <div class="w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center"> | |
| <i data-lucide="users" class="w-5 h-5 text-indigo-600"></i> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Action Bar --> | |
| <div class="flex gap-3 mb-4"> | |
| <button onclick="clearAll()" class="px-4 py-2 bg-red-50 text-red-600 rounded-lg text-sm font-medium hover:bg-red-100 transition flex items-center gap-2"> | |
| <i data-lucide="trash-2" class="w-4 h-4"></i> | |
| Clear All | |
| </button> | |
| <button onclick="exportData()" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200 transition flex items-center gap-2"> | |
| <i data-lucide="download" class="w-4 h-4"></i> | |
| Export | |
| </button> | |
| <button onclick="loadQueue()" class="px-4 py-2 bg-indigo-50 text-indigo-600 rounded-lg text-sm font-medium hover:bg-indigo-100 transition flex items-center gap-2 ml-auto"> | |
| <i data-lucide="refresh-cw" class="w-4 h-4"></i> | |
| Refresh | |
| </button> | |
| </div> | |
| <!-- Queue List --> | |
| <div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> | |
| <div class="px-6 py-4 border-b border-gray-200 bg-gray-50 flex items-center justify-between"> | |
| <h2 class="font-semibold text-gray-800">Current Queue</h2> | |
| <span class="text-xs text-gray-500 bg-white px-2 py-1 rounded border">Updates automatically</span> | |
| </div> | |
| <div id="queueList" class="divide-y divide-gray-100"> | |
| <!-- Queue items will be inserted here --> | |
| </div> | |
| <div id="emptyState" class="hidden py-12 text-center text-gray-500"> | |
| <i data-lucide="inbox" class="w-12 h-12 mx-auto mb-3 text-gray-300"></i> | |
| <p>No tickets in queue</p> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Toast Notification --> | |
| <div id="toast" class="hidden fixed bottom-4 right-4 bg-gray-800 text-white px-6 py-3 rounded-lg shadow-lg transform transition-all duration-300 translate-y-20"> | |
| <div class="flex items-center gap-2"> | |
| <i data-lucide="info" class="w-5 h-5"></i> | |
| <span id="toastMessage">Notification</span> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize icons | |
| lucide.createIcons(); | |
| // Update clock | |
| function updateClock() { | |
| const now = new Date(); | |
| document.getElementById('clock').textContent = now.toLocaleTimeString('en-US', { hour12: false }); | |
| document.getElementById('date').textContent = now.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); | |
| } | |
| setInterval(updateClock, 1000); | |
| updateClock(); | |
| // Load and display queue | |
| function loadQueue() { | |
| const queue = JSON.parse(localStorage.getItem('techSupportQueue') || '[]'); | |
| const queueList = document.getElementById('queueList'); | |
| const emptyState = document.getElementById('emptyState'); | |
| // Update stats | |
| const waiting = queue.filter(t => t.status === 'waiting').length; | |
| const serving = queue.filter(t => t.status === 'serving').length; | |
| const completed = queue.filter(t => t.status === 'completed').length; | |
| document.getElementById('statWaiting').textContent = waiting; | |
| document.getElementById('statServing').textContent = serving; | |
| document.getElementById('statCompleted').textContent = completed; | |
| document.getElementById('statTotal').textContent = queue.length; | |
| if (queue.length === 0) { | |
| queueList.innerHTML = ''; | |
| emptyState.classList.remove('hidden'); | |
| return; | |
| } | |
| emptyState.classList.add('hidden'); | |
| // Sort by timestamp (oldest first for FCFS) | |
| const sortedQueue = [...queue].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); | |
| queueList.innerHTML = sortedQueue.map((ticket, index) => { | |
| const timeAgo = getTimeAgo(new Date(ticket.timestamp)); | |
| const statusColors = { | |
| waiting: 'bg-orange-100 text-orange-800 border-orange-200', | |
| serving: 'bg-blue-100 text-blue-800 border-blue-200', | |
| completed: 'bg-green-100 text-green-800 border-green-200' | |
| }; | |
| const techColors = { | |
| Gary: 'bg-blue-50 text-blue-700 border-blue-200', | |
| Will: 'bg-green-50 text-green-700 border-green-200', | |
| Tim: 'bg-purple-50 text-purple-700 border-purple-200', | |
| Anyone: 'bg-orange-50 text-orange-700 border-orange-200' | |
| }; | |
| return ` | |
| <div class="queue-item p-4 hover:bg-gray-50 transition flex items-center justify-between gap-4"> | |
| <div class="flex items-center gap-4 flex-1"> | |
| <div class="flex flex-col items-center justify-center w-12"> | |
| <span class="text-2xl font-bold text-gray-300">#${index + 1}</span> | |
| </div> | |
| <div class="flex-1"> | |
| <div class="flex items-center gap-2 mb-1"> | |
| <span class="font-semibold text-gray-900">${ticket.name}</span> | |
| <span class="text-xs text-gray-400">• ${timeAgo}</span> | |
| </div> | |
| <div class="flex items-center gap-2 flex-wrap"> | |
| <span class="px-2 py-1 rounded-full text-xs font-medium border ${techColors[ticket.technician] || 'bg-gray-100 text-gray-700'}"> | |
| ${ticket.technician} | |
| </span> | |
| <span class="px-2 py-1 rounded-full text-xs font-medium border ${statusColors[ticket.status]} status-badge"> | |
| ${ticket.status.charAt(0).toUpperCase() + ticket.status.slice(1)} | |
| </span> | |
| <span class="text-xs text-gray-500 font-mono">${ticket.checkInTime}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| ${ticket.status === 'waiting' ? ` | |
| <button onclick="updateStatus(${ticket.id}, 'serving')" class="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition" title="Start Serving"> | |
| <i data-lucide="play" class="w-5 h-5"></i> | |
| </button> | |
| ` : ''} | |
| ${ticket.status === 'serving' ? ` | |
| <button onclick="updateStatus(${ticket.id}, 'completed')" class="p-2 text-green-600 hover:bg-green-50 rounded-lg transition" title="Complete"> | |
| <i data-lucide="check" class="w-5 h-5"></i> | |
| </button> | |
| ` : ''} | |
| <button onclick="deleteTicket(${ticket.id})" class="p-2 text-red-600 hover:bg-red-50 rounded-lg transition" title="Remove"> | |
| <i data-lucide="x" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| lucide.createIcons(); | |
| } | |
| function getTimeAgo(date) { | |
| const seconds = Math.floor((new Date() - date) / 1000); | |
| if (seconds < 60) return 'Just now'; | |
| const minutes = Math.floor(seconds / 60); | |
| if (minutes < 60) return `${minutes}m ago`; | |
| const hours = Math.floor(minutes / 60); | |
| if (hours < 24) return `${hours}h ago`; | |
| return `${Math.floor(hours / 24)}d ago`; | |
| } | |
| function updateStatus(id, status) { | |
| let queue = JSON.parse(localStorage.getItem('techSupportQueue') || '[]'); | |
| const ticket = queue.find(t => t.id === id); | |
| if (ticket) { | |
| ticket.status = status; | |
| localStorage.setItem('techSupportQueue', JSON.stringify(queue)); | |
| loadQueue(); | |
| showToast(`Ticket marked as ${status}`); | |
| } | |
| } | |
| function deleteTicket(id) { | |
| if (confirm('Remove this ticket from the queue?')) { | |
| let queue = JSON.parse(localStorage.getItem('techSupportQueue') || '[]'); | |
| queue = queue.filter(t => t.id !== id); | |
| localStorage.setItem('techSupportQueue', JSON.stringify(queue)); | |
| loadQueue(); | |
| showToast('Ticket removed'); | |
| } | |
| } | |
| function clearAll() { | |
| if (confirm('Are you sure you want to clear all tickets?')) { | |
| localStorage.setItem('techSupportQueue', '[]'); | |
| loadQueue(); | |
| showToast('All tickets cleared'); | |
| } | |
| } | |
| function exportData() { | |
| const queue = JSON.parse(localStorage.getItem('techSupportQueue') || '[]'); | |
| if (queue.length === 0) { | |
| showToast('No data to export'); | |
| return; | |
| } | |
| const csv = [ | |
| ['Name', 'Technician', 'Check-in Time', 'Status', 'Timestamp'].join(','), | |
| ...queue.map(t => [ | |
| t.name, | |
| t.technician, | |
| t.checkInTime, | |
| t.status, | |
| t.timestamp | |
| ].join(',')) | |
| ].join('\n'); | |
| const blob = new Blob([csv], { type: 'text/csv' }); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `tech-support-queue-${new Date().toISOString().split('T')[0]}.csv`; | |
| a.click(); | |
| showToast('Data exported to CSV'); | |
| } | |
| function showToast(message) { | |
| const toast = document.getElementById('toast'); | |
| document.getElementById('toastMessage').textContent = message; | |
| toast.classList.remove('translate-y-20', 'hidden'); | |
| setTimeout(() => { | |
| toast.classList.add('translate-y-20'); | |
| }, 3000); | |
| } | |
| // Auto-refresh every 5 seconds | |
| setInterval(loadQueue, 5000); | |
| // Initial load | |
| loadQueue(); | |
| </script> | |
| </body> | |
| </html> |