/** * Payfrit HUD - Heads Up Display for Task Management * * Displays tasks as vertical bars that grow over 60 seconds. * Bars flash when they exceed the target time. * Workers can tap to view details or long-press to accept. */ const HUD = { // Configuration config: { pollInterval: 1000, // Poll every second for smooth animation targetSeconds: 60, // Target time to accept a task apiBaseUrl: '/api/tasks', // API endpoint businessId: parseInt(new URLSearchParams(window.location.search).get('b')) || 17, }, // State tasks: [], selectedTask: null, longPressTimer: null, isConnected: true, businessName: '', // Category names (will be loaded from API) categories: { 1: { name: 'Orders', color: '#ef4444' }, 2: { name: 'Pickup', color: '#f59e0b' }, 3: { name: 'Delivery', color: '#22c55e' }, 4: { name: 'Support', color: '#3b82f6' }, 5: { name: 'Kitchen', color: '#8b5cf6' }, 6: { name: 'Other', color: '#ec4899' }, }, // Initialize init() { this.updateClock(); setInterval(() => this.updateClock(), 1000); setInterval(() => this.updateBars(), 1000); setInterval(() => this.fetchTasks(), 3000); // Initial fetch (also gets business name) this.fetchTasks(); // Close overlay on background tap document.getElementById('taskOverlay').addEventListener('click', (e) => { if (e.target.id === 'taskOverlay') { this.closeOverlay(); } }); console.log('[HUD] Initialized'); }, // Update the clock display updateClock() { const now = new Date(); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); document.getElementById('clock').textContent = `${hours}:${minutes}:${seconds}`; }, // Fetch tasks from API async fetchTasks() { try { const response = await fetch(`${this.config.apiBaseUrl}/listPending.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: this.config.businessId }) }); const data = await response.json(); if (data.OK) { this.tasks = data.TASKS || []; this.renderTasks(); this.setConnected(true); // Update business name from response if (data.BUSINESS_NAME && !this.businessName) { this.businessName = data.BUSINESS_NAME; document.getElementById('businessName').textContent = ' - ' + this.businessName; } } else { console.error('[HUD] API error:', data.ERROR); this.setConnected(false); } } catch (err) { console.error('[HUD] Fetch error:', err); this.setConnected(false); // Demo mode: generate fake tasks if API unavailable if (this.tasks.length === 0) { this.loadDemoTasks(); } } }, // Load demo tasks for testing loadDemoTasks() { const now = Date.now(); this.tasks = [ { TaskID: 101, TaskCategoryID: 1, TaskTitle: 'Order #42', TaskCreatedOn: new Date(now - 15000).toISOString() }, { TaskID: 102, TaskCategoryID: 3, TaskTitle: 'Order #43', TaskCreatedOn: new Date(now - 35000).toISOString() }, { TaskID: 103, TaskCategoryID: 2, TaskTitle: 'Pickup #7', TaskCreatedOn: new Date(now - 55000).toISOString() }, { TaskID: 104, TaskCategoryID: 5, TaskTitle: 'Prep #12', TaskCreatedOn: new Date(now - 70000).toISOString() }, ]; this.renderTasks(); }, // Render task bars renderTasks() { const container = document.getElementById('taskContainer'); const emptyState = document.getElementById('emptyState'); // Show/hide empty state if (this.tasks.length === 0) { emptyState.style.display = 'block'; } else { emptyState.style.display = 'none'; } // Get existing bar elements const existingBars = container.querySelectorAll('.task-bar'); const existingIds = new Set(); existingBars.forEach(bar => existingIds.add(parseInt(bar.dataset.taskId))); // Get current task IDs const currentIds = new Set(this.tasks.map(t => t.TaskID)); // Remove bars for tasks that no longer exist existingBars.forEach(bar => { const id = parseInt(bar.dataset.taskId); if (!currentIds.has(id)) { bar.remove(); } }); // Add or update bars this.tasks.forEach(task => { let bar = container.querySelector(`.task-bar[data-task-id="${task.TaskID}"]`); if (!bar) { bar = this.createTaskBar(task); container.appendChild(bar); } this.updateTaskBar(bar, task); }); }, // Create a new task bar element createTaskBar(task) { const bar = document.createElement('div'); bar.className = 'task-bar'; bar.dataset.taskId = task.TaskID; bar.dataset.category = task.TaskCategoryID || 1; // Apply dynamic category color const categoryColor = this.getCategoryColor(task); bar.style.setProperty('--bar-color', categoryColor); bar.innerHTML = ` #${task.TaskID} ${this.getCategoryName(task)} `; // Touch/click handlers bar.addEventListener('mousedown', (e) => this.onBarPress(task, e)); bar.addEventListener('mouseup', () => this.onBarRelease()); bar.addEventListener('mouseleave', () => this.onBarRelease()); bar.addEventListener('touchstart', (e) => this.onBarPress(task, e)); bar.addEventListener('touchend', () => this.onBarRelease()); bar.addEventListener('click', () => this.onBarTap(task)); return bar; }, // Update task bar height and state updateTaskBar(bar, task) { const elapsed = this.getElapsedSeconds(task.TaskCreatedOn); const percentage = Math.min(elapsed / this.config.targetSeconds * 100, 100); bar.style.height = `${percentage}%`; // Update time display const timeEl = bar.querySelector('.task-time'); if (timeEl) { timeEl.textContent = this.formatElapsed(elapsed); } // Flash if overdue if (elapsed >= this.config.targetSeconds) { bar.classList.add('flashing'); } else { bar.classList.remove('flashing'); } }, // Update all bars (called every second) updateBars() { const bars = document.querySelectorAll('.task-bar'); bars.forEach(bar => { const taskId = parseInt(bar.dataset.taskId); const task = this.tasks.find(t => t.TaskID === taskId); if (task) { this.updateTaskBar(bar, task); } }); }, // Get elapsed seconds since task creation getElapsedSeconds(createdOn) { const created = new Date(createdOn); const now = new Date(); return Math.floor((now - created) / 1000); }, // Format elapsed time as mm:ss formatElapsed(seconds) { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${String(secs).padStart(2, '0')}`; }, // Get category name - use task's category name if available, fall back to hardcoded getCategoryName(task) { if (task && task.TaskCategoryName) { return task.TaskCategoryName; } return this.categories[task?.TaskCategoryID]?.name || 'Task'; }, // Get category color - use task's category color if available, fall back to hardcoded getCategoryColor(task) { if (task && task.TaskCategoryColor) { return task.TaskCategoryColor; } return this.categories[task?.TaskCategoryID]?.color || '#888888'; }, // Handle bar press (start long press timer) onBarPress(task, e) { e.preventDefault(); this.longPressTimer = setTimeout(() => { this.acceptTaskDirect(task); }, 800); // 800ms for long press }, // Handle bar release (cancel long press) onBarRelease() { if (this.longPressTimer) { clearTimeout(this.longPressTimer); this.longPressTimer = null; } }, // Handle bar tap (show details) onBarTap(task) { if (this.longPressTimer) { clearTimeout(this.longPressTimer); this.longPressTimer = null; } this.showTaskDetails(task); }, // Show task details overlay showTaskDetails(task) { this.selectedTask = task; const overlay = document.getElementById('taskOverlay'); const detail = document.getElementById('taskDetail'); const elapsed = this.getElapsedSeconds(task.TaskCreatedOn); const categoryName = this.getCategoryName(task); const categoryColor = this.getCategoryColor(task); document.getElementById('detailTitle').textContent = task.TaskTitle || `Task #${task.TaskID}`; document.getElementById('detailCategory').textContent = categoryName; document.getElementById('detailCreated').textContent = new Date(task.TaskCreatedOn).toLocaleTimeString(); document.getElementById('detailWaiting').textContent = this.formatElapsed(elapsed); document.getElementById('detailInfo').textContent = task.TaskDetails || '-'; detail.style.setProperty('--detail-color', categoryColor); overlay.classList.add('visible'); }, // Close overlay closeOverlay() { document.getElementById('taskOverlay').classList.remove('visible'); this.selectedTask = null; }, // Accept task from overlay acceptTask() { if (this.selectedTask) { this.acceptTaskDirect(this.selectedTask); this.closeOverlay(); } }, // Accept task directly (long press or button) async acceptTaskDirect(task) { console.log('[HUD] Accepting task:', task.TaskID); try { const response = await fetch(`${this.config.apiBaseUrl}/accept.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ TaskID: task.TaskID, BusinessID: this.config.businessId }) }); const data = await response.json(); if (data.OK) { // Remove from local list immediately this.tasks = this.tasks.filter(t => t.TaskID !== task.TaskID); this.renderTasks(); this.showFeedback('Task accepted!', 'success'); } else { this.showFeedback(data.ERROR || 'Failed to accept', 'error'); } } catch (err) { console.error('[HUD] Accept error:', err); // Demo mode: just remove locally this.tasks = this.tasks.filter(t => t.TaskID !== task.TaskID); this.renderTasks(); this.showFeedback('Task accepted!', 'success'); } }, // Show feedback message showFeedback(message, type) { // Simple console feedback for now console.log(`[HUD] ${type}: ${message}`); // TODO: Add toast notification }, // Set connection status setConnected(connected) { this.isConnected = connected; const indicator = document.getElementById('statusIndicator'); if (connected) { indicator.classList.remove('disconnected'); } else { indicator.classList.add('disconnected'); } } }; // Global functions for onclick handlers function closeOverlay() { HUD.closeOverlay(); } function acceptTask() { HUD.acceptTask(); } // Initialize on load document.addEventListener('DOMContentLoaded', () => { HUD.init(); });