`; const iw = new google.maps.InfoWindow({ content: content }); m.addListener('click', () => iw.open(map, m)); markers.push(m); }); jobMarkers.forEach(m => m.setMap(null)); jobMarkers = []; if(showJobs) d.jobs.forEach(j => { const jm = new google.maps.Marker({ position: {lat:j.lat, lng:j.lng}, map:map, label:{text:j.id.toString(), color:'#fff', fontWeight:'bold', fontSize:'11px'}, icon:{ path: google.maps.SymbolPath.CIRCLE, scale: 12, fillColor: "#1a1c1e", fillOpacity: 1, strokeColor: "#ff8c00", strokeWeight: 2 } }); jm.addListener('click', () => { new google.maps.InfoWindow({ content: `
SITE ${j.id}: ${j.name}
${j.addr}
` }).open(map, jm); }); jobMarkers.push(jm); }); }); callBackend('getStaffList', {}, BID).then(list => { localStaff = list; renderStaff(); }); callBackend('getAllJobs', {}, BID).then(list => { localJobs = list; renderJobs(); }); } function teleportToStaff(name) { const m = markers.find(x => x.staffName === name); if (m) { map.setCenter(m.getPosition()); map.setZoom(18); navClick('map'); } else alert("No GPS fix available."); } function teleportToSite(lat, lng) { map.setCenter({lat: parseFloat(lat), lng: parseFloat(lng)}); map.setZoom(17); navClick('map'); } // --- SORTING LOGIC: STAFF --- function sortStaff(type) { if (type === 'az') localStaff.sort((a,b) => a.name.localeCompare(b.name)); if (type === 'company') localStaff.sort((a,b) => a.company.localeCompare(b.company)); renderStaff(); saveStaffOrder(); } function moveCardUp(idx) { if (idx <= 0) return; const temp = localStaff[idx]; localStaff[idx] = localStaff[idx - 1]; localStaff[idx - 1] = temp; renderStaff(); saveStaffOrder(); } function moveCardDown(idx) { if (idx >= localStaff.length - 1) return; const temp = localStaff[idx]; localStaff[idx] = localStaff[idx + 1]; localStaff[idx + 1] = temp; renderStaff(); saveStaffOrder(); } function saveStaffOrder() { const names = localStaff.map(s => s.name); callBackend('reorderStaff', {namesArray: names}, BID); } // --- SORTING LOGIC: JOBS --- function sortJobs(type) { if (type === 'az') localJobs.sort((a,b) => a.name.localeCompare(b.name)); renderJobs(); saveJobOrder(); } function moveJobUp(idx) { if (idx <= 0) return; const temp = localJobs[idx]; localJobs[idx] = localJobs[idx - 1]; localJobs[idx - 1] = temp; renderJobs(); saveJobOrder(); } function moveJobDown(idx) { if (idx >= localJobs.length - 1) return; const temp = localJobs[idx]; localJobs[idx] = localJobs[idx + 1]; localJobs[idx + 1] = temp; renderJobs(); saveJobOrder(); } function saveJobOrder() { const names = localJobs.map(j => j.name); callBackend('reorderJobs', {namesArray: names}, BID); } function triggerReminder(email, name, pin) { if (!email || email === 'undefined') { alert("No email found on file for this user."); return; } alert("Sending reminder to " + email + "..."); callBackend('sendReminderEmail', {email: email, name: name, pin: pin}, BID).then(res => { if(res.success) alert("Reminder sent successfully!"); else alert("Failed to send: " + res.msg); }); } function renderStaff() { const q = document.getElementById('roster-search').value.toLowerCase(); let h = ''; localStaff.forEach((w, index) => { if(q && !w.name.toLowerCase().includes(q)) return; const color = (w.status === 'Signed In') ? 'var(--success)' : 'var(--error)'; h += `
${w.status}
Site Hrs: ${w.hoursToday}
${w.name.toUpperCase()}
${w.company.toUpperCase()} | ${w.role}
✉️ ${w.email} | 📞 ${w.contact}
PIN: ${w.pin}
IN: ${w.lastIn} | OUT: ${w.lastOut}
TELEPORT
${w.isActive ? 'DEACTIVATE' : 'ACTIVATE'}
REMOVE
✉ SEND REMINDER
`; }); document.getElementById('roster-list').innerHTML = h; setupDragDrop('roster-list', 'reorderStaff', 'data-name'); } function renderJobs() { let h = ''; localJobs.forEach((j, index) => { h += `
SITE ${j.id}: ${j.name.toUpperCase()}
${j.addr}
TELEPORT
${j.status === 'Active' ? 'DEACTIVATE' : 'ACTIVATE'}
DELETE
`; }); document.getElementById('jobs-list').innerHTML = h; setupDragDrop('jobs-list', 'reorderJobs', 'data-name'); } function addStaff() { var o = {name:document.getElementById('sn').value, email:document.getElementById('se').value, company:document.getElementById('sc').value, role:document.getElementById('sr').value, contact:document.getElementById('sk').value, pin:document.getElementById('sp').value}; callBackend('saveStaffMember', o, BID).then(res => { if(res.success) { sync(); ['sn','se','sc','sr','sk','sp'].forEach(i => document.getElementById(i).value = ''); } else { alert("Failed to add staff member. " + (res.msg || "")); } }); } function saveJob() { var o = {name:document.getElementById('j-name').value, address:document.getElementById('j-addr').value, lat:document.getElementById('j-lat').value, lng:document.getElementById('j-lng').value}; callBackend('saveNewJob', o, BID).then(res => { if(res.success) { sync(); ['j-name','j-addr','j-coords-display'].forEach(i => document.getElementById(i).value = ''); } else { alert("Failed to save site. " + (res.msg || "")); } }); } function navClick(t) { document.querySelectorAll('.nav-item').forEach(x => x.classList.remove('active')); document.querySelectorAll('.drawer').forEach(x => x.style.display = 'none'); if (t === 'map') document.getElementById('m-btn').classList.add('active'); else if (t === 'jobs') { document.getElementById('jobs-drawer').style.display = 'block'; document.getElementById('j-btn').classList.add('active'); } else if (t === 'staff') { document.getElementById('staff-drawer').style.display = 'block'; document.getElementById('s-btn').classList.add('active'); } else if (t === 'leaderboard') { document.getElementById('leaderboard-drawer').style.display = 'block'; document.getElementById('lb-btn').classList.add('active'); loadLeaderboard(); } else if (t === 'today') { loadLogs('today', document.getElementById('t-btn')); } else if (t === 'stats') { document.getElementById('stats-drawer').style.display = 'block'; document.getElementById('stats-btn').classList.add('active'); loadStats(); } } function loadLeaderboard() { callBackend('getLeaderboardData', {}, BID).then(data => { leaderboardData = data; const sel = document.getElementById('week-selector'); sel.innerHTML = ''; Object.keys(data).sort().reverse().forEach(w => sel.innerHTML += ``); renderLeaderboard(); }); } function renderLeaderboard() { const week = document.getElementById('week-selector').value; let h = ''; if(leaderboardData[week]) leaderboardData[week].forEach((p, i) => { let rC = (i===0)?'rank-1':(i===1)?'rank-2':(i===2)?'rank-3':''; h += `
${i+1}
${p.name}
${p.score} pts
`; }); document.getElementById('lb-content').innerHTML = h; } function loadLogs(day, el) { document.getElementById('log-panel').style.display = 'block'; el.classList.add('active'); var actionStr = (day === 'today') ? 'getTodayLogs' : 'getYesterdayLogs'; callBackend(actionStr, {}, BID).then(logs => { var h = ''; logs.forEach(r => h += ``); document.getElementById('log-content').innerHTML = h + '
TIMESTAFFACTION
${new Date(r[0]).toLocaleTimeString()}${r[1]}${r[2]}
'; }); } function loadStats() { callBackend('getDashboard', {}, BID).then(s => { document.getElementById('stat-active-jobs').innerText = s.activeJobs; document.getElementById('stat-onsite').innerText = s.onSite; }); } function setupDragDrop(lI, sF, aN) { const l = document.getElementById(lI); l.addEventListener('dragover', e => { e.preventDefault(); const d = document.querySelector('.dragging'); if (d) l.appendChild(d); }); l.addEventListener('dragend', () => { const i = Array.from(l.querySelectorAll('.item-card')).map(el => el.getAttribute(aN)); callBackend(sF, {namesArray: i}, BID); }); } function drag(e) { e.target.classList.add('dragging'); } function enablePinning() { isPinning = true; document.getElementById('map').classList.add('crosshair-mode'); } function disablePinning() { isPinning = false; document.getElementById('map').classList.remove('crosshair-mode'); } function setCoords(la, ln, ad) { document.getElementById('j-lat').value = la; document.getElementById('j-lng').value = ln; document.getElementById('j-addr').value = ad; document.getElementById('j-coords-display').value = parseFloat(la).toFixed(5) + "," + parseFloat(ln).toFixed(5); } function generatePin() { document.getElementById('sp').value = Math.floor(100000 + Math.random() * 900000); } function downloadCSV() { callBackend('getCsvData', {}, BID).then(d => { var b = new Blob([d.map(e => e.join(",")).join("\n")], {type: 'text/csv'}); var a = document.createElement("a"); a.href = URL.createObjectURL(b); a.download = "Logs.csv"; a.click(); }); } function goBackToOperator() { window.location.href = "Operator.html"; }

SITERoll ADMIN

00
PERSONNEL
SITES
SHOW NAMES

ADD PERSONNEL

X

ROSTER

ESTABLISH SITE

X

DIRECTORY

DASHBOARD STATS

X
Active Jobs: 0
On Site Today: 0

LEADERBOARD

X
SCORING RULES:
1 pt for every minute on site.
+10 pts Bonus: Sign in before 7:00 AM.
+10 pts Bonus: Sign out after 3:30 PM.
-20 pts Penalty: Sign in after 7:00 AM.
-20 pts Penalty: Sign out before 3:30 PM.
* Failure to sign out forfeits all points for that session.

LOGS

X