Artemis
Tactical Security
Sign in as
Password
CONNECTING TO CLOUD
Syncing data across all devices…
Notifications
No notifications yet
// ─── PATROL STATS ───────────────────────────────────────────────────────────── // Keep old schedule functions for any remaining references function renderSchedule() { renderLiveDashboard(); } function renderSchedulePeriodChips() {} function selectSchedulePeriod() {} function renderScheduleTable() {} function periodDays(p) { const days = []; let d = new Date(p.start + 'T00:00:00'); const end = new Date(p.end + 'T00:00:00'); while (d <= end) { days.push(new Date(d)); d.setDate(d.getDate() + 1); } return days; } // ─── PRICES ─────────────────────────────────────────────────────────────────── function toggleAccordion(id) { const el = document.getElementById(id); if (!el) return; el.classList.toggle('open'); } function openSettings() { if (!isAdminLevel()) return; renderSettingsPrices(); renderSettingsSites(); document.getElementById('settings-overlay').style.display = 'block'; requestAnimationFrame(() => document.getElementById('settings-drawer').classList.add('open')); } function closeSettings() { document.getElementById('settings-drawer').classList.remove('open'); setTimeout(() => { document.getElementById('settings-overlay').style.display = 'none'; }, 300); } function renderSettingsSites() { const list = document.getElementById('settings-site-list'); const defaults = document.getElementById('settings-default-sites'); const sub = document.getElementById('acc-sites-sub'); if (sub) sub.textContent = customSites.length > 0 ? customSites.length + ' custom · ' + PREDEFINED_SITES.length + ' default' : PREDEFINED_SITES.length + ' default sites'; if (defaults) defaults.innerHTML = PREDEFINED_SITES.join(' · '); if (!list) return; if (customSites.length === 0) { list.innerHTML = '
No custom sites added yet.
'; return; } list.innerHTML = customSites.map((s, i) => `
📍
${s}
`).join(''); } function addCustomSite() { const input = document.getElementById('settings-new-site'); const name = input ? input.value.trim() : ''; if (!name) { showToast('Enter a site name'); return; } const allSites = [...PREDEFINED_SITES, ...customSites]; if (allSites.some(s => s.toLowerCase() === name.toLowerCase())) { showToast('Site already exists'); return; } customSites.push(name); if (input) input.value = ''; save(); renderSettingsSites(); showToast('Site added ✓'); } function removeCustomSite(idx) { customSites.splice(idx, 1); save(); renderSettingsSites(); showToast('Site removed'); } function getAllSites() { return [...PREDEFINED_SITES, ...customSites]; } function renderSettingsPrices() { const list = document.getElementById('settings-price-list'); const totalEl = document.getElementById('settings-price-total'); if (!list) return; list.innerHTML = uniforms.map((u, i) => `
${u.name}
${u.category}
PGK
`).join(''); updateSettingsPriceTotal(); } function updateSettingsPriceTotal() { const inputs = document.querySelectorAll('#settings-price-list .price-input'); let total = 0; inputs.forEach(inp => { total += parseFloat(inp.value) || 0; }); const el = document.getElementById('settings-price-total'); if (el) el.textContent = 'PGK ' + total.toFixed(2); } function savePricesFromSettings() { const inputs = document.querySelectorAll('#settings-price-list .price-input'); inputs.forEach(inp => { const i = parseInt(inp.dataset.idx); if (!isNaN(i)) uniforms[i].price = parseFloat(inp.value) || 0; }); save(); renderPrices(); // keep main prices view in sync showToast('Prices saved ✓'); closeSettings(); } function renderPrices() { const isAdmin = isAdminLevel(); let total = 0; const el = document.getElementById('price-list'); el.innerHTML = uniforms.map((u, i) => { total += u.price; return `
${u.name}
${u.category}
${isAdmin ? `` : `
${fmtPGK(u.price)}
`}
`; }).join(''); document.getElementById('price-total').textContent = fmtPGK(total); } function previewTotal() { let total = 0; uniforms.forEach((u, i) => { const inp = document.getElementById('price-input-' + i); if (inp) total += parseFloat(inp.value)||0; }); document.getElementById('price-total').textContent = fmtPGK(total); } function savePrices() { uniforms.forEach((u, i) => { const inp = document.getElementById('price-input-' + i); if (inp) u.price = parseFloat(inp.value)||0; }); save(); showToast('Prices updated ✓'); renderPrices(); } // ─── AMENDMENT REQUESTS ────────────────────────────────────────────────────── function requestAmendment() { if (!currentGuard) return; const g = guards.find(x => x.id === currentGuard); const p = PERIODS.find(x => x.id === currentPeriod); const inspector = document.getElementById('ip-inspector-input').value || 'Operator'; if (!amendmentRequests[currentPeriod]) amendmentRequests[currentPeriod] = {}; amendmentRequests[currentPeriod][currentGuard] = { status: 'pending', requestedAt: new Date().toISOString(), requestedBy: inspector, }; // Notify admin notifications.unshift({ id: Date.now(), type: 'amend_request', title: 'Amendment requested', body: `${g?.name || currentGuard} · ${p?.label} · Requested by ${inspector}`, time: new Date().toISOString(), read: false, periodId: currentPeriod, guardId: currentGuard, }); save(); updateNotifBell(); // Update button const amendBtn = document.getElementById('ip-amend-btn'); amendBtn.textContent = 'Amendment requested — awaiting approval'; amendBtn.disabled = true; showToast('Amendment request sent to admin'); } function approveAmendment(periodId, guardId, notifId) { // Unlock the inspection if (lockedInspections[periodId]) delete lockedInspections[periodId][guardId]; if (amendmentRequests[periodId]) delete amendmentRequests[periodId][guardId]; // Add approval notification const g = guards.find(x => x.id === guardId); const p = PERIODS.find(x => x.id === periodId); notifications.unshift({ id: Date.now(), type: 'amend_approved', title: 'Amendment approved', body: `${g?.name || guardId} · ${p?.label} — operator can now edit`, time: new Date().toISOString(), read: false, periodId, guardId, }); markNotifRead(notifId); save(); renderNotifList(); updateNotifBell(); showToast('Amendment approved — inspection unlocked'); } function denyAmendment(periodId, guardId, notifId) { if (amendmentRequests[periodId]?.[guardId]) { amendmentRequests[periodId][guardId].status = 'denied'; } markNotifRead(notifId); save(); renderNotifList(); showToast('Amendment request denied'); } // ─── NOTIFICATIONS ──────────────────────────────────────────────────────────── function updateNotifBell() { const unread = notifications.filter(n => !n.read).length; document.getElementById('notif-dot').classList.toggle('show', unread > 0); } function markNotifRead(id) { const n = notifications.find(x => x.id === id); if (n) n.read = true; save(); updateNotifBell(); } function markAllRead() { notifications.forEach(n => n.read = true); save(); updateNotifBell(); renderNotifList(); } function toggleNotifPanel() { const panel = document.getElementById('notif-panel'); const overlay = document.getElementById('notif-overlay'); const isOpen = panel.classList.contains('open'); if (!isOpen) { renderNotifList(); panel.classList.add('open'); overlay.classList.add('show'); } else { panel.classList.remove('open'); overlay.classList.remove('show'); } } function renderNotifList() { const el = document.getElementById('notif-list'); if (notifications.length === 0) { el.innerHTML = '
No notifications yet
'; return; } el.innerHTML = notifications.slice(0, 30).map(n => { const dt = new Date(n.time); const timeStr = dt.toLocaleDateString('en-PG', {day:'numeric',month:'short'}) + ' ' + dt.toLocaleTimeString('en-PG', {hour:'2-digit',minute:'2-digit'}); const isPending = n.type === 'amend_request' && amendmentRequests[n.periodId]?.[n.guardId]?.status === 'pending'; const icon = n.type === 'saved' ? '💾' : n.type === 'amend_request' ? '✏️' : '✅'; return `
${icon} ${n.title}
${n.body}
${timeStr}
${!n.read ? '' : ''}
${isPending && isAdminLevel() ? `
` : ''}
`; }).join(''); } // ─── INIT ───────────────────────────────────────────────────────────────────── // ─── GPS GEO-TAG ────────────────────────────────────────────────────────────── function captureGPS() { if (!navigator.geolocation) { setGeoState('error', '📵', 'GPS not supported', 'Your device does not support geolocation.', ''); return; } setGeoState('loading', '⏳', 'Acquiring GPS…', 'Please wait, locating device…', ''); const btn = document.getElementById('geo-btn'); if (btn) { btn.textContent = '⏳ Locating…'; btn.disabled = true; } navigator.geolocation.getCurrentPosition( (pos) => { const { latitude, longitude, accuracy } = pos.coords; document.getElementById('pl-geo-lat').value = latitude; document.getElementById('pl-geo-lng').value = longitude; document.getElementById('pl-geo-acc').value = accuracy || ''; setGeoState( 'acquired', '✅', 'GPS captured', `${latitude.toFixed(6)}, ${longitude.toFixed(6)}`, accuracy ? `Accuracy: ±${Math.round(accuracy)}m` : '' ); const btn = document.getElementById('geo-btn'); if (btn) { btn.textContent = '🔄 Refresh'; btn.disabled = false; } }, (err) => { let msg = 'Location access denied.'; if (err.code === 2) msg = 'GPS signal unavailable.'; if (err.code === 3) msg = 'Location request timed out.'; setGeoState('error', '❌', 'GPS failed', msg, 'Tap to retry'); const btn = document.getElementById('geo-btn'); if (btn) { btn.textContent = '📡 Retry'; btn.disabled = false; } }, { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 } ); } function setGeoState(state, icon, label, coords, acc) { const box = document.getElementById('geo-tag-box'); const iconEl = document.getElementById('geo-icon'); const labelEl = document.getElementById('geo-label'); const coordsEl = document.getElementById('geo-coords'); const accEl = document.getElementById('geo-acc'); if (!box) return; box.className = 'geo-tag-box ' + (state === 'acquired' ? 'acquired' : state === 'error' ? 'error' : state === 'loading' ? 'loading' : ''); if (iconEl) iconEl.textContent = icon; if (labelEl) labelEl.textContent = label; if (coordsEl) coordsEl.textContent = coords; if (accEl) accEl.textContent = acc; } function resetGeoUI() { document.getElementById('pl-geo-lat').value = ''; document.getElementById('pl-geo-lng').value = ''; document.getElementById('pl-geo-acc').value = ''; setGeoState('', '📍', 'Location not captured', 'Tap to capture GPS coordinates', ''); const btn = document.getElementById('geo-btn'); if (btn) { btn.textContent = '📡 Get GPS'; btn.disabled = false; } } // ─── LIVE GEO MAP ───────────────────────────────────────────────────────────── function renderLiveGeoMap() { const mapEl = document.getElementById('live-geo-map'); const feedEl = document.getElementById('live-geo-feed'); if (!mapEl) return; // Gather all patrol logs with geo const geoLogs = patrolLogs.filter(p => p.geo && p.geo.lat && p.geo.lng); if (geoLogs.length === 0) { mapEl.innerHTML = '
🛰️
NO GPS DATA YET
Save a patrol log with GPS to see map
'; if (feedEl) feedEl.innerHTML = ''; return; } // Build OpenStreetMap iframe centered on most recent const recent = geoLogs[0]; const lat = recent.geo.lat; const lng = recent.geo.lng; const zoom = 14; // Build markers as URL params for OpenStreetMap // Use a leaflet-based embed via a data URL const now = Date.now(); const ONE_HOUR = 3600000; const TODAY_START = new Date(); TODAY_START.setHours(0,0,0,0); const markers = geoLogs.slice(0, 20).map(p => { const age = now - new Date(p.loggedAt).getTime(); const color = age < ONE_HOUR ? '#4ade80' : new Date(p.loggedAt) >= TODAY_START ? '#38bdf8' : '#1e3a5f'; return { lat: p.geo.lat, lng: p.geo.lng, color, label: p.officer || p.guardName || '?', site: p.site || '', time: p.loggedAt, acc: p.geo.acc }; }); const markersJson = JSON.stringify(markers); const leafletHTML = `