diff --git a/static/index.html b/static/index.html
index 76b5fce..882f96c 100644
--- a/static/index.html
+++ b/static/index.html
@@ -827,16 +827,6 @@
.map-search-row input {
flex: 1; min-width: 0;
}
- .map-filter-chips {
- display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 6px;
- }
- .map-filter-chip {
- font-size: 10px; font-weight: 600; padding: 3px 8px;
- border-radius: 12px; border: 1px solid var(--border);
- background: var(--card2); color: var(--text2); cursor: pointer;
- transition: all 0.12s;
- }
- .map-filter-chip.active { background: var(--accent); border-color: var(--accent); color: #fff; }
/* Asset list below map */
.map-asset-list {
@@ -1281,6 +1271,22 @@
+
+
+
📷 Photo
+
+
![Asset photo]()
+
+
+
+
+
+
![]()
+
✕
+
+
📍 Directions & Access
@@ -1378,8 +1384,86 @@
oninput="mapDebounceFilter()">
-
-
+
+
+
+
+
+
+
+
Category
+
+
+
+
+
+
+
Location Type
+
+
+
+
Disney Park
+
+
+
+
+
+
Customer
+
+
+
+
Location
+
+
+
+
+
+
Branch
+
+
+
+
Assigned To
+
+
+
+
+
+
+
+
+
@@ -3831,6 +3915,18 @@
df('Created', formatDate(a.created_at)) +
df('Updated', formatDate(a.updated_at));
+ // Photo
+ const photoCard = document.getElementById('detailPhotoCard');
+ const photoImg = document.getElementById('detailPhotoImg');
+ if (a.photo_path) {
+ photoCard.style.display = 'block';
+ photoImg.src = a.photo_path;
+ photoImg.onload = () => { photoImg.style.display = 'inline'; };
+ photoImg.onerror = () => { photoCard.style.display = 'none'; };
+ } else {
+ photoCard.style.display = 'none';
+ }
+
// Location / Directions
const hasDir = a.address || a.building_name || a.building_number || a.floor || a.room || a.trailer_number || a.walking_directions || a.map_link || a.parking_location;
const dirCard = document.getElementById('detailDirections');
@@ -4016,6 +4112,34 @@
}).catch(() => {});
}
+ // ── Photo viewer ──────────────────────────────────────────────────
+ function openPhotoModal() {
+ const img = document.getElementById('detailPhotoImg');
+ const modal = document.getElementById('photoModal');
+ const modalImg = document.getElementById('photoModalImg');
+ if (!img || !modal || !modalImg) return;
+ modalImg.src = img.src;
+ modal.style.display = 'flex';
+ document.body.style.overflow = 'hidden';
+ }
+ function closePhotoModal(event) {
+ if (event && event.target && event.target.id !== 'photoModal' && event.target.id !== 'photoModalImg' && event.target.tagName !== 'SPAN') return;
+ const modal = document.getElementById('photoModal');
+ if (modal) {
+ modal.style.display = 'none';
+ document.body.style.overflow = '';
+ }
+ }
+ document.addEventListener('keydown', function(e) {
+ if (e.key === 'Escape') {
+ const modal = document.getElementById('photoModal');
+ if (modal && modal.style.display === 'flex') {
+ modal.style.display = 'none';
+ document.body.style.overflow = '';
+ }
+ }
+ });
+
async function navigateToAsset(mode) {
mode = mode || 'driving';
if (!AppState.currentAssetId) {
@@ -4497,81 +4621,252 @@
}
+ // ── Map filter state ──────────────────────────────────────────────────
+ let mapFilters = { category: null, status: null, make: null, disney_park: null, disney_filter: null, q: '',
+ customer_id: null, location_id: null, assigned_to: null, branch: null };
+ let mapFilterDropdownsLoaded = false;
+
+ function toggleMapFilterPanel() {
+ const panel = document.getElementById('mapFilterPanel');
+ const toggle = document.getElementById('mapFilterToggle');
+ panel.classList.toggle('open');
+ toggle.classList.toggle('active', panel.classList.contains('open'));
+ if (panel.classList.contains('open') && !mapFilterDropdownsLoaded) {
+ loadMapFilterDropdowns();
+ }
+ }
+
+ async function loadMapFilterDropdowns() {
+ if (mapFilterDropdownsLoaded) return;
+ mapFilterDropdownsLoaded = true;
+ var panel = document.getElementById('mapFilterPanel');
+ var loadingMsg = document.createElement('div');
+ loadingMsg.className = 'loading-spinner';
+ loadingMsg.innerHTML = ' Loading filters...';
+ panel.appendChild(loadingMsg);
+ try {
+ var cats = await api('/api/categories');
+ document.getElementById('mapFilterCategory').innerHTML = '' +
+ cats.map(function(c) { return ''; }).join('');
+
+ var makes = await api('/api/makes');
+ document.getElementById('mapFilterMake').innerHTML = '' +
+ makes.map(function(m) { return ''; }).join('');
+
+ var customers = await api('/api/customers');
+ document.getElementById('mapFilterCustomer').innerHTML = '' +
+ customers.map(function(c) { return ''; }).join('');
+
+ var users = await api('/api/users');
+ document.getElementById('mapFilterAssignedTo').innerHTML = '' +
+ users.filter(function(u) { return u.role === 'technician' || u.role === 'admin'; })
+ .map(function(u) { return ''; }).join('');
+
+ var branches = await api('/api/branches');
+ document.getElementById('mapFilterBranch').innerHTML = '' +
+ branches.branches.map(function(b) { return ''; }).join('');
+ } catch (e) {
+ showToast('Failed to load filter options', true);
+ } finally {
+ if (loadingMsg && loadingMsg.parentNode) loadingMsg.parentNode.removeChild(loadingMsg);
+ }
+ }
+
+ function onMapFilterChange() {
+ mapFilters.category = document.getElementById('mapFilterCategory').value || null;
+ mapFilters.make = document.getElementById('mapFilterMake').value || null;
+
+ var disneyFilterVal = document.getElementById('mapFilterDisneyFilter').value;
+ if (disneyFilterVal) {
+ mapFilters.disney_filter = disneyFilterVal;
+ mapFilters.disney_park = null;
+ } else {
+ var parkVal = document.getElementById('mapFilterDisneyPark').value;
+ mapFilters.disney_park = parkVal || null;
+ mapFilters.disney_filter = null;
+ }
+ mapFilters.customer_id = document.getElementById('mapFilterCustomer').value || null;
+
+ var locEl = document.getElementById('mapFilterLocation');
+ var prevCust = locEl.dataset.customerId;
+ var custEl = document.getElementById('mapFilterCustomer');
+ if (custEl.value !== prevCust) {
+ locEl.innerHTML = '';
+ locEl.disabled = true;
+ mapFilters.location_id = null;
+ if (custEl.value) {
+ loadMapLocationsForCustomer(custEl.value);
+ }
+ } else {
+ mapFilters.location_id = locEl.value || null;
+ }
+ locEl.dataset.customerId = custEl.value;
+
+ mapFilters.assigned_to = document.getElementById('mapFilterAssignedTo').value || null;
+ mapFilters.branch = document.getElementById('mapFilterBranch').value || null;
+
+ renderMapActiveFilterTags();
+ applyMapFilter();
+ }
+
+ async function loadMapLocationsForCustomer(customerId) {
+ try {
+ var locations = await api('/api/locations?customer_id=' + customerId);
+ var locEl = document.getElementById('mapFilterLocation');
+ locEl.innerHTML = '' +
+ locations.map(function(l) { return ''; }).join('');
+ locEl.disabled = false;
+ } catch (e) { showToast('Failed to load locations', true); }
+ }
+
+ function getMapActiveFilterCount() {
+ var count = 0;
+ if (mapFilters.category) count++;
+ if (mapFilters.make) count++;
+ if (mapFilters.disney_park) count++;
+ if (mapFilters.disney_filter) count++;
+ if (mapFilters.customer_id) count++;
+ if (mapFilters.location_id) count++;
+ if (mapFilters.assigned_to) count++;
+ if (mapFilters.branch) count++;
+ if (mapFilters.q) count++;
+ return count;
+ }
+
+ function renderMapActiveFilterTags() {
+ var el = document.getElementById('mapActiveFilters');
+ var countEl = document.getElementById('mapFilterCount');
+ var toggle = document.getElementById('mapFilterToggle');
+ var count = getMapActiveFilterCount();
+
+ countEl.textContent = count;
+ countEl.style.display = count > 0 ? 'inline' : 'none';
+ if (toggle) toggle.classList.toggle('active', count > 0);
+
+ var tags = [];
+ if (mapFilters.q) tags.push({key:'q', label:'🔍 "' + mapFilters.q + '"'});
+ if (mapFilters.category) tags.push({key:'category', label:'📂 ' + mapFilters.category});
+ if (mapFilters.make) tags.push({key:'make', label:'🏭 ' + mapFilters.make});
+ if (mapFilters.disney_park) tags.push({key:'disney_park', label: parkIcon(mapFilters.disney_park) + ' ' + parkName(mapFilters.disney_park)});
+ if (mapFilters.disney_filter === 'disney') tags.push({key:'disney_filter', label:'🏰 Disney only'});
+ if (mapFilters.disney_filter === 'non_disney') tags.push({key:'disney_filter', label:'🏢 Non-Disney'});
+ if (mapFilters.customer_id) {
+ var custSel = document.getElementById('mapFilterCustomer');
+ var txt = custSel.options[custSel.selectedIndex] ? custSel.options[custSel.selectedIndex].text : 'Customer';
+ tags.push({key:'customer_id', label:'🟢 ' + txt});
+ }
+ if (mapFilters.location_id) {
+ var locSel = document.getElementById('mapFilterLocation');
+ var txt = locSel.options[locSel.selectedIndex] ? locSel.options[locSel.selectedIndex].text : 'Location';
+ tags.push({key:'location_id', label:'📍 ' + txt});
+ }
+ if (mapFilters.assigned_to) tags.push({key:'assigned_to', label:'👤 ' + mapFilters.assigned_to});
+ if (mapFilters.branch) tags.push({key:'branch', label:'🏢 ' + mapFilters.branch});
+
+ el.innerHTML = tags.map(function(t) {
+ return '' + esc(t.label) + ' ✕';
+ }).join('');
+ }
+
+ function removeMapFilter(key) {
+ if (key === 'q') {
+ document.getElementById('mapSearchInput').value = '';
+ mapFilters.q = '';
+ } else if (key === 'category') {
+ document.getElementById('mapFilterCategory').value = '';
+ mapFilters.category = null;
+ } else if (key === 'make') {
+ document.getElementById('mapFilterMake').value = '';
+ mapFilters.make = null;
+ } else if (key === 'disney_park') {
+ document.getElementById('mapFilterDisneyPark').value = '';
+ mapFilters.disney_park = null;
+ } else if (key === 'disney_filter') {
+ document.getElementById('mapFilterDisneyFilter').value = '';
+ mapFilters.disney_filter = null;
+ } else if (key === 'customer_id') {
+ document.getElementById('mapFilterCustomer').value = '';
+ document.getElementById('mapFilterLocation').innerHTML = '';
+ document.getElementById('mapFilterLocation').disabled = true;
+ mapFilters.customer_id = null;
+ mapFilters.location_id = null;
+ } else if (key === 'location_id') {
+ document.getElementById('mapFilterLocation').value = '';
+ mapFilters.location_id = null;
+ } else if (key === 'assigned_to') {
+ document.getElementById('mapFilterAssignedTo').value = '';
+ mapFilters.assigned_to = null;
+ } else if (key === 'branch') {
+ document.getElementById('mapFilterBranch').value = '';
+ mapFilters.branch = null;
+ }
+ renderMapActiveFilterTags();
+ applyMapFilter();
+ }
+
+ function clearMapFilters() {
+ mapFilters = { category: null, status: null, make: null, disney_park: null, disney_filter: null, q: '',
+ customer_id: null, location_id: null, assigned_to: null, branch: null };
+ document.getElementById('mapSearchInput').value = '';
+ document.getElementById('mapFilterCategory').value = '';
+ document.getElementById('mapFilterMake').value = '';
+ document.getElementById('mapFilterDisneyPark').value = '';
+ document.getElementById('mapFilterDisneyFilter').value = '';
+ document.getElementById('mapFilterCustomer').value = '';
+ document.getElementById('mapFilterLocation').innerHTML = '';
+ document.getElementById('mapFilterLocation').disabled = true;
+ document.getElementById('mapFilterAssignedTo').value = '';
+ document.getElementById('mapFilterBranch').value = '';
+ renderMapActiveFilterTags();
+ applyMapFilter();
+ }
+
async function loadAssetPins() {
- // Store markers on a namespace so we can clear them
if (!window._assetPins) window._assetPins = [];
- // Clear existing markers
window._assetPins.forEach(function(m) { if (map) map.removeLayer(m); });
window._assetPins = [];
try {
const assets = await api('/api/assets?has_gps=true&limit=2000');
_mapAllAssets = assets;
- buildMapFilterChips(assets);
- // Apply current filter
applyMapFilter();
} catch (e) {
console.error('Failed to load asset pins:', e);
+ showToast('Failed to load map assets — ' + (e.message || 'connection error'), true);
}
}
- function buildMapFilterChips(assets) {
- var cats = {}; var statuses = {};
- assets.forEach(function(a) {
- if (a.category) cats[a.category] = (cats[a.category] || 0) + 1;
- if (a.status) statuses[a.status] = (statuses[a.status] || 0) + 1;
- });
- var catKeys = Object.keys(cats).sort();
- var statusKeys = Object.keys(statuses).sort();
- var html = '';
- html += 'All';
- catKeys.forEach(function(c) {
- html += '' + esc(c) + ' (' + cats[c] + ')';
- });
- html += 'All Status';
- statusKeys.forEach(function(s) {
- html += '' + esc(s) + '';
- });
- document.getElementById('mapFilterChips').innerHTML = html;
- }
-
- function mapSetCategory(cat) {
- _mapActiveCategory = _mapActiveCategory === cat ? '' : cat;
- _mapActiveStatus = '';
- applyMapFilter();
- buildMapFilterChips(_mapAllAssets);
- }
-
- function mapSetStatus(status) {
- _mapActiveStatus = _mapActiveStatus === status ? '' : status;
- _mapActiveCategory = '';
- applyMapFilter();
- buildMapFilterChips(_mapAllAssets);
- }
-
function mapDebounceFilter() {
if (_mapFilterTimer) clearTimeout(_mapFilterTimer);
+ mapFilters.q = (document.getElementById('mapSearchInput').value || '').toLowerCase().trim();
+ renderMapActiveFilterTags();
_mapFilterTimer = setTimeout(applyMapFilter, 200);
}
function applyMapFilter() {
if (_mapFilterTimer) { clearTimeout(_mapFilterTimer); _mapFilterTimer = null; }
- var q = (document.getElementById('mapSearchInput').value || '').toLowerCase().trim();
+ var q = mapFilters.q;
// Clear existing pins
window._assetPins.forEach(function(m) { if (map) map.removeLayer(m); });
window._assetPins = [];
var matched = [];
- _mapAllAssets.forEach(function(a) {
+ var allAssets = _mapAllAssets || [];
+ allAssets.forEach(function(a) {
// Category filter
- if (_mapActiveCategory && a.category !== _mapActiveCategory) return;
- // Status filter
- if (_mapActiveStatus && a.status !== _mapActiveStatus) return;
+ if (mapFilters.category && a.category !== mapFilters.category) return;
+ // Make filter
+ if (mapFilters.make && a.make !== mapFilters.make) return;
+ // Disney/non-disney filter
+ if (mapFilters.disney_filter === 'disney' && !a.is_disney) return;
+ if (mapFilters.disney_filter === 'non_disney' && a.is_disney) return;
+ // Disney park filter
+ if (mapFilters.disney_park && a.disney_park !== mapFilters.disney_park) return;
// Text search
if (q) {
- var haystack = ((a.name || '') + ' ' + (a.machine_id || '') + ' ' + (a.category || '') + ' ' + (a.description || '')).toLowerCase();
+ var haystack = ((a.name || '') + ' ' + (a.machine_id || '') + ' ' + (a.category || '') + ' ' + (a.company || '') + ' ' + (a.description || '')).toLowerCase();
if (haystack.indexOf(q) === -1) return;
}
matched.push(a);
@@ -4619,7 +4914,6 @@
el.innerHTML = '📦 No assets match your search
';
return;
}
- // Sort by name
var sorted = assets.slice().sort(function(a, b) { return (a.name || '').localeCompare(b.name || ''); });
var html = '';
sorted.forEach(function(a) {
diff --git a/static/sw.js b/static/sw.js
index e2caaf5..b10e404 100644
--- a/static/sw.js
+++ b/static/sw.js
@@ -3,7 +3,7 @@
// Caches app shell + CDN deps. Network-first for API, cache fallback for static.
// ═══════════════════════════════════════════════════════════════════════════
-const CACHE_NAME = 'canteen-v6';
+const CACHE_NAME = 'canteen-v7';
// App shell — core resources needed to boot the PWA
const APP_SHELL = [
diff --git a/tests/test_map_frontend.py b/tests/test_map_frontend.py
index 994c9ea..09e02cb 100644
--- a/tests/test_map_frontend.py
+++ b/tests/test_map_frontend.py
@@ -142,8 +142,8 @@ class TestGPSControls:
assert "GPS location not available" in body()
def test_pins_chip_ui(self):
- """Pin toggle chip exists."""
- assert "chipPins" in body()
+ """🎯 Filters toggle button exists in map tab."""
+ assert "mapFilterToggle" in body()
class TestHeatmap: