My Tasks

Worker
W
Loading tasks...
en: { brand: "Worker", navTasks: "My Tasks", brand: "Worker", navTasks: "My Tasks", navHistory: "History", navStats: "My Stats", statusLabel: "Current Status", avail: "Available", busy: "Busy", start: "Start Work", assigned: "ASSIGNED", finish: "Finish & Upload", settings: "Settings", language: "Language", theme: "Theme", close: "Close", viewmap: "View Map", cancel: "Cancel", signout: "Sign Out", goBusy: "Go Busy", goFree: "Go Available", closeMap: "Close Map" }, hi: { brand: "कर्मचारी", navTasks: "मेरे कार्य", brand: "कर्मचारी", navTasks: "मेरे कार्य", navHistory: "इतिहास", navStats: "सांख्यिकी", statusLabel: "वर्तमान स्थिति", avail: "उपलब्ध", busy: "व्यस्त", start: "काम शुरू करें", assigned: "आवंटित", finish: "समाप्त करें और अपलोड करें", settings: "सेटिंग्स", language: "भाषा", theme: "थीम", close: "बंद करें", viewmap: "मानचित्र देखें", cancel: "रद्द करें", signout: "साइन आउट", goBusy: "व्यस्त हो जाएँ", goFree: "उपलब्ध कराएँ", closeMap: "मानचित्र बंद करें" }, ta: { brand: "தொழிலாளி", navTasks: "எனது பணி", navHistory: "வரலாறு", statusLabel: "தற்போதைய நிலை", avail: "கிடைக்கும்", busy: "பொழுதுபோக்கு", start: "வேலைத் தொடங்கு", assigned: "ஒதுக்கப்பட்டுள்ளது", finish: "முடித்துவிட்டு பதிவேற்று", settings: "அமைப்புகள்", language: "மொழி", theme: "தீம்", close: "மூடு", viewmap: "வரைபடம் காண்", cancel: "ரத்து", signout: "வெளியேறு", goBusy: "பிஸியாக்க", goFree: "கிடைக்கும்", closeMap: "வரைபடம் மூடு" }, te: { brand: "తన పని", navTasks: "నాకు పని", navHistory: "చరిత్ర", statusLabel: "ప్రస్తుత స్థితి", avail: "అందుబాటులో ఉంది", busy: "బిజీ", start: "పని ప్రారంభించు", assigned: "నియుక్తం", finish: "ముగించి అప్లోడ్ చేయండి", settings: "సెట్టింగ్స్", language: "భాష", theme: "థీమ్", close: "క్లోజ్", viewmap: "మ్యాప్ చూడండి", cancel: "రద్దు", signout: "సైన్ అవుట్", goBusy: "బిజీ అవ్వండి", goFree: "నిల్వకు రాబోసు", closeMap: "మ్యాప్ మూసేయి" }, kn: { brand: "ಕೆಲಸಗಾರ", navTasks: "ನನ್ನ ಕಾರ್ಯಗಳು", navHistory: "ಇತಿಹಾಸ", statusLabel: "ಪ್ರಸ್ತುತ ಸ್ಥಿತಿ", avail: "ಲಭ್ಯವಿದೆ", busy: "ವ್ಯಸ್ತ", start: "ಕೆಲಸ ಪ್ರಾರಂಭಿಸಿ", assigned: "ಹೊಂದಿಸಲಾಗಿದೆ", finish: "ಪೂರ್ಣ ಮಾಡಿ ಅಪ್‍ಲೋಡ್ ಮಾಡಿ", settings: "ಸೆಟಿಂಗ್‍ಗಳು", language: "ಭಾಷೆ", theme: "ಥೀಂ", close: "ಮುಚ್ಚಿ", viewmap: "ನಕ್ಷೆ ನೋಡಿ", cancel: "ರದ್ದುಗೊಳಿಸಿ", signout: "ಸೈನ್ ಔಟ್", goBusy: "ವ್ಯಸ್ತರಾಗಿ", goFree: "ಲಭ್ಯವಾಗಿರಿ", closeMap: "ನಕ್ಷೆ ಮುಚ್ಚು" }, ml: { brand: "ജോലിക്കാരൻ", navTasks: "എന്റെ ജോലികൾ", navHistory: "ചരിത്രം", statusLabel: "വർത്തമാന നില", avail: "ലഭ്യമാണ്", busy: "ബിസി", start: "ജോലി ആരംഭിക്കുക", assigned: "അയവിറുത്തിയിരിക്കുന്നു", finish: "അവസാനിച്ച് അപ്‌ലോഡ് ചെയ്യുക", settings: "ക്രമീകരണങ്ങൾ", language: "ഭാഷ", theme: "തീം", close: "മൂടുക", viewmap: "മാപ്പ് കാണുക", cancel: "റദ്ദാക്കുക", signout: "സൈൻ ഔട്ട്", goBusy: "ബിസിയാവുക", goFree: "ലഭ്യമാക്കുക", closeMap: "മാപ്പ് അടയ്ക്കുക" }, bn: { brand: "কর্মী", navTasks: "আমার কাজ", navHistory: "ইতিহাস", statusLabel: "বর্তমান অবস্থা", avail: "উপলব্ধ", busy: "ব্যস্ত", start: "কাজ শুরু করুন", assigned: "নিয়োগকৃত", finish: "সমাপ্ত করুন এবং আপলোড করুন", settings: "সেটিংস", language: "ভাষা", theme: "থিম", close: "বন্ধ করুন", viewmap: "মানচিত্র দেখুন", cancel: "বাতিল", signout: "সাইন আউট", goBusy: "ব্যস্ত হন", goFree: "উপলব্ধ করুন", closeMap: "মানচিত্র বন্ধ করুন" }, mr: { brand: "कामगार", navTasks: "माझी कामे", navHistory: "इतिहास", statusLabel: "सध्याची स्थिती", avail: "उपलब्ध", busy: "बिझी", start: "काम सुरू करा", assigned: "नेमलेले", finish: "पूर्ण करा आणि अपलोड करा", settings: "सेटिंग्ज", language: "भाषा", theme: "थीम", close: "बंद करा", viewmap: "नकाशा पहा", cancel: "रद्द करा", signout: "साइन आउट", goBusy: "बिझी व्हा", goFree: "उपलब्ध करा", closeMap: "नकाशा बंद करा" } }; // Helper to apply translations to all [data-key] elements function applyTranslations(lang) { const map = TRANSLATIONS[lang] || TRANSLATIONS['en']; document.querySelectorAll('[data-key]').forEach(el => { const key = el.getAttribute('data-key'); if (map[key]) { el.textContent = map[key]; } }); // set HTML lang attribute for accessibility/screen readers document.documentElement.lang = lang; // Update status button text as it is dynamic updateStatusButtonText(); // Update the Finish button text specifically (data-key="finish") const finishButtons = document.querySelectorAll('.btn-finish'); finishButtons.forEach(btn => btn.textContent = map.finish || TRANSLATIONS['en'].finish); } function changeLanguage(lang) { currentLang = lang; applyTranslations(lang); } // Initialize with English applyTranslations('en'); function updateStatusButtonText() { const btn = document.getElementById('statusBtn'); if (!btn) return; const t = TRANSLATIONS[currentLang] || TRANSLATIONS['en']; btn.textContent = isAvailable ? (t.goBusy || 'Go Busy') : (t.goFree || 'Go Available'); } function toggleStatus(forceBusy) { if (forceBusy === true) isAvailable = false; else if (typeof forceBusy === 'boolean') isAvailable = !forceBusy; else isAvailable = !isAvailable; const txt = document.getElementById('statusText'); if (isAvailable) { txt.textContent = TRANSLATIONS[currentLang]?.avail || 'Available'; txt.style.color = 'var(--accent)'; document.getElementById('statusBtn').className = 'toggle-btn is-avail'; } else { txt.textContent = TRANSLATIONS[currentLang]?.busy || 'Busy'; txt.style.color = '#ff3b3b'; document.getElementById('statusBtn').className = 'toggle-btn is-busy'; } updateStatusButtonText(); } // NAV function showTab(tab) { if (tab === 'tasks') { document.getElementById('tasksTab').style.display = 'block'; document.getElementById('historyTab').classList.remove('active'); document.querySelector('[data-key="navTasks"]').classList.add('active'); document.querySelector('[data-key="navHistory"]').classList.remove('active'); } else { document.getElementById('tasksTab').style.display = 'none'; document.getElementById('historyTab').classList.add('active'); document.querySelector('[data-key="navTasks"]').classList.remove('active'); document.querySelector('[data-key="navHistory"]').classList.add('active'); } } // Click outside to close dropdown document.addEventListener('click', (e) => { const profile = document.getElementById('workerProfileBtn'); const dropdown = document.getElementById('profileDropdown'); if (!profile.contains(e.target)) dropdown.classList.remove('show'); }); /* SECURE AUTHENTICATION */ function getSession() { try { const sessionStr = localStorage.getItem('tarfixer_session'); if (!sessionStr) return null; const signedSession = JSON.parse(sessionStr); // Extract actual session data from signed wrapper return signedSession.data || signedSession; } catch (e) { return null; } } async function signOut(e) { if (e) { e.stopPropagation(); e.preventDefault(); } // Close dropdown immediately document.getElementById('profileDropdown').classList.remove('show'); if (confirm((TRANSLATIONS[currentLang]?.signout || 'Sign Out') + '?')) { try { await API.logout(); } catch (err) { console.error('Logout error:', err); } // Clear any remaining session data localStorage.removeItem('tarfixer_auth_token'); localStorage.removeItem('tarfixer_user_data'); localStorage.removeItem('tarfixer_session'); window.location.href = '../Login/Choose_login.html'; } } function loadWorkerInfo() { const user = API.getUser(); console.log('Loading worker info, user:', user); // Try to get name from multiple sources let displayName = 'Worker'; let email = ''; if (user) { email = user.email || ''; // Try different name sources displayName = user.name || user.displayName || email.split('@')[0] || 'Worker'; } // Also check localStorage directly in case API.getUser() is stale const storedUser = localStorage.getItem('tarfixer_user_data'); if (storedUser) { try { const parsed = JSON.parse(storedUser); if (parsed.name) displayName = parsed.name; if (!email && parsed.email) email = parsed.email; } catch (e) { } } document.getElementById('workerName').textContent = displayName; document.getElementById('workerEmail').textContent = email; // Generate initials const initials = displayName !== 'Worker' ? displayName.split(' ').map(n => n[0]).join('').toUpperCase().substring(0, 2) : 'W'; document.getElementById('workerInitials').textContent = initials; console.log('Worker info set:', { displayName, email, initials }); } async function protectDashboard() { // Check if authenticated if (!API.isAuthenticated()) { console.warn('Not authenticated, redirecting to login'); window.location.replace('../Login/Choose_login.html?error=not_authenticated'); return false; } // Check user type from stored data const user = API.getUser(); if (!user || user.userType !== 'worker') { console.warn('Access denied: not a worker'); await API.logout(); window.location.replace('../Login/Choose_login.html?error=access_denied'); return false; } console.log('Worker authenticated, loading dashboard...'); return true; } // SETTINGS modal open/close function openSettings(e) { if (e) e.stopPropagation(); document.getElementById('profileDropdown').classList.remove('show'); document.getElementById('settingsModal').style.display = 'flex'; document.getElementById('settingsModal').setAttribute('aria-hidden', 'false'); setTimeout(() => document.getElementById('langSelect').focus(), 60); } function closeSettings() { document.getElementById('settingsModal').style.display = 'none'; document.getElementById('settingsModal').setAttribute('aria-hidden', 'true'); } // Click outside settings to close (backdrop) const settingsModal = document.getElementById('settingsModal'); settingsModal.addEventListener('click', (e) => { if (e.target === settingsModal) closeSettings(); }); // THEME function toggleTheme() { document.body.classList.toggle('light-theme'); if (taskMap) { taskMap.removeLayer(tileLayerModal); // Keep Satellite view for "Real Map" feel // Keep Google Maps view consistent const url = 'http://mt0.google.com/vt/lyrs=m&hl=en&x={x}&y={y}&z={z}'; tileLayerModal = L.tileLayer(url, { maxZoom: 20 }).addTo(taskMap); } } // MAP modal logic function showTaskMap(lat, lng, id) { const modal = document.getElementById('mapModal'); modal.style.display = 'flex'; modal.setAttribute('aria-hidden', 'false'); if (taskMap) { try { taskMap.remove(); } catch (e) { } taskMap = null; tileLayerModal = null; document.getElementById('mapModalInner').innerHTML = ''; } const isInvalid = !lat && !lng || (lat === 0 && lng === 0); const displayLat = isInvalid ? 11.1271 : lat; const displayLng = isInvalid ? 78.6569 : lng; setTimeout(() => { taskMap = L.map('mapModalInner', { attributionControl: false }).setView([displayLat, displayLng], isInvalid ? 7 : 15); // Use Google Maps (Roadmap) tiles as requested const url = 'http://mt0.google.com/vt/lyrs=m&hl=en&x={x}&y={y}&z={z}'; const attribution = '© Google Maps'; tileLayerModal = L.tileLayer(url, { maxZoom: 20, attribution: attribution }).addTo(taskMap); if (isInvalid) { L.marker([displayLat, displayLng]) .addTo(taskMap) .bindPopup("Location Data Missing
Showing default location (Tamil Nadu)") .openPopup(); } else { L.circleMarker([displayLat, displayLng], { radius: 8, fillColor: '#ff9800', color: '#fff', weight: 2, fillOpacity: 1 }) .addTo(taskMap) .bindPopup(id) .openPopup(); } setTimeout(() => taskMap.invalidateSize(), 120); }, 80); } function closeMapModal() { const modal = document.getElementById('mapModal'); modal.style.display = 'none'; modal.setAttribute('aria-hidden', 'true'); if (taskMap) { try { taskMap.remove(); } catch (e) { } taskMap = null; tileLayerModal = null; document.getElementById('mapModalInner').innerHTML = ''; } } // close map when clicking backdrop const mapModal = document.getElementById('mapModal'); mapModal.addEventListener('click', (e) => { if (e.target === mapModal) closeMapModal(); }); // ESC behavior: close modals / camera document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { if (document.getElementById('mapModal').style.display === 'flex') closeMapModal(); if (document.getElementById('settingsModal').style.display === 'flex') closeSettings(); if (document.getElementById('camModal').style.display === 'flex') closeCamera(); } }); // START WORK -> show Finish button and set busy // START WORK: update status to in_progress async function startWork(taskId) { try { const token = getToken(); const response = await fetch(`${API_BASE_URL}/reports/${taskId}/status`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'in_progress' }) }); if (!response.ok) { throw new Error('Failed to update task status'); } // Refresh tasks to show updated status await fetchMyTasks(); toggleStatus(true); // set worker busy } catch (error) { console.error('Error starting work:', error); alert('Failed to start work. Please try again.'); } } // FINISH & UPLOAD flow function finishAndUpload(taskId) { currentFinishingTask = taskId; openCameraForFinal(); } // CAMERA functions (final photo only) function openCameraForFinal() { const camModal = document.getElementById('camModal'); camModal.style.display = 'flex'; camModal.setAttribute('aria-hidden', 'false'); const video = document.getElementById('cameraView'); navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false }) .then(stream => { activeCameraStream = stream; video.srcObject = stream; video.play().catch(() => { }); }) .catch(err => { alert('Camera access denied or not available.'); closeCamera(); }); } function closeCamera() { const camModal = document.getElementById('camModal'); camModal.style.display = 'none'; camModal.setAttribute('aria-hidden', 'true'); const video = document.getElementById('cameraView'); if (video && video.srcObject) { const tracks = video.srcObject.getTracks(); tracks.forEach(t => t.stop()); } activeCameraStream = null; } // Capture final photo and verify GPS + YOLO before upload async function captureFinalPhoto() { const video = document.getElementById('cameraView'); if (!video || !video.srcObject) { alert('Camera not ready.'); return; } const canvas = document.createElement('canvas'); canvas.width = video.videoWidth || 1280; canvas.height = video.videoHeight || 720; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const dataUrl = canvas.toDataURL('image/jpeg', 0.85); if (!navigator.geolocation) { alert('GPS required to verify location. Enable location and try again.'); return; } // Show analyzing message const camControls = document.querySelector('.cam-controls'); const originalControls = camControls.innerHTML; camControls.innerHTML = '
🔍 Analyzing repaired road...
Please wait while AI verifies the repair
'; // Get current GPS position navigator.geolocation.getCurrentPosition(async (pos) => { const currentLat = pos.coords.latitude; const currentLng = pos.coords.longitude; // Get original task location and damage info const taskCard = document.getElementById(`task-${currentFinishingTask}`); const originalLat = parseFloat(taskCard?.dataset.lat || 0); const originalLng = parseFloat(taskCard?.dataset.lng || 0); // Calculate distance between current location and task location (in meters) const distance = calculateDistance(currentLat, currentLng, originalLat, originalLng); // Allow 500 meters tolerance (adjust as needed) const MAX_DISTANCE = 500; if (distance > MAX_DISTANCE) { camControls.innerHTML = originalControls; closeCamera(); alert(`❌ Location verification failed!\n\nYou are ${distance.toFixed(0)}m away from the task location.\nMaximum allowed distance: ${MAX_DISTANCE}m\n\nPlease go to the correct location and try again.`); return; } // GPS verified - now verify with YOLO that damage is repaired try { const token = getToken(); // First, analyze the after image with YOLO camControls.innerHTML = '
🤖 Running YOLO damage detection...
Verifying repair quality
'; // Convert dataUrl to blob for YOLO API const blob = await fetch(dataUrl).then(r => r.blob()); const formData = new FormData(); formData.append('image', blob, 'after_image.jpg'); const yoloResponse = await fetch(`${API_BASE_URL}/detect`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData }); let afterDamagePercentage = 0; let yoloVerified = false; let yoloMessage = ''; if (yoloResponse.ok) { const yoloData = await yoloResponse.json(); afterDamagePercentage = yoloData.damage_percentage || 0; // Repair is verified if damage is below 30% const MAX_ALLOWED_DAMAGE = 30; if (afterDamagePercentage < MAX_ALLOWED_DAMAGE) { yoloVerified=true; yoloMessage=`✅ Repair verified! Remaining damage: ${afterDamagePercentage.toFixed(1)}%`; } else { yoloVerified=false; yoloMessage=`❌ Repair incomplete! Detected damage: ${afterDamagePercentage.toFixed(1)}%\nDamage must be below ${MAX_ALLOWED_DAMAGE}% to mark as complete.`; } } else { // YOLO failed - allow submission but flag it yoloVerified=true; yoloMessage='⚠️ Could not verify with AI (service unavailable). Submitting for manual review.' ; afterDamagePercentage=-1; // Flag as unverified } if (!yoloVerified) { camControls.innerHTML=originalControls; closeCamera(); alert(yoloMessage + '\n\nPlease complete the repair work and try again.' ); return; } // YOLO verified - proceed with upload camControls.innerHTML='
📤 Uploading completion...
' ; // Update report status to 'done' with the after image const response=await fetch(`${API_BASE_URL}/reports/${currentFinishingTask}/complete`, { method: 'POST' , headers: { 'Authorization' : `Bearer ${token}`, 'Content-Type' : 'application/json' }, body: JSON.stringify({ after_image: dataUrl, completion_lat: currentLat, completion_lng: currentLng, completion_time: new Date().toISOString(), after_damage_percentage: afterDamagePercentage }) }); closeCamera(); if (response.ok) { const damageMsg=afterDamagePercentage>= 0 ? `AI Verification: ${afterDamagePercentage.toFixed(1)}% damage remaining` : 'Pending manual review'; alert(`✅ Task completed successfully!\n\n📍 Location verified: ${distance.toFixed(0)}m from task.\n🤖 ${damageMsg}\n📷 Photo uploaded for officer review.`); // Add to history const historyTab = document.getElementById('historyTab'); const timeLabel = new Date().toLocaleString(); historyTab.insertAdjacentHTML('afterbegin', `
TF-${currentFinishingTask.substring(0, 6)}
Completed
${timeLabel}
`); // Refresh tasks (completed one should be removed) await fetchMyTasks(); toggleStatus(false); // set worker available } else { const errorData = await response.json(); alert('❌ Failed to submit completion: ' + (errorData.error || 'Unknown error')); } } catch (error) { console.error('Error completing task:', error); closeCamera(); alert('❌ Failed to complete task. Please try again.'); } currentFinishingTask = null; }, (err) => { alert('GPS permission denied. Please enable location and try again.'); }, { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }); } // Calculate distance between two GPS coordinates (Haversine formula) function calculateDistance(lat1, lon1, lat2, lon2) { const R = 6371000; // Earth's radius in meters const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; // Distance in meters } function markTaskCompleted(taskId, time) { // This is now handled by fetchMyTasks() refresh } // Accessibility / small fixes: prevent clicks inside modal content from closing modal document.querySelectorAll('.modal, .map-modal .box').forEach(el => { el.addEventListener('click', e => e.stopPropagation()); }); // INIT: set initial translations applyTranslations('en'); // Protect dashboard and load user info on page load window.addEventListener('load', async () => { // Clear redirect flag since we successfully loaded the dashboard sessionStorage.removeItem('just_redirected'); // Protect dashboard - only workers can access const isProtected = await protectDashboard(); if (!isProtected) { return; } // Load worker information loadWorkerInfo(); // Fetch tasks assigned to this worker await fetchMyTasks(); });