ux: Map tab enhancements + toast replacements

- Replace 9 native alert()/confirm() calls with showToast()
- Add search bar over map (name, machine_id, category, description)
- Add filter chips with category counts (Bev, Food, Snack, etc.)
- Make map container square (aspect-ratio 1/1)
- Add asset list below map showing filtered results
- Increase asset limit from 1000 to 2000
- Fix missing closing brace in .geofence-panel CSS
- Zero console errors on all tabs
This commit is contained in:
2026-05-27 19:33:43 -04:00
parent e66bf2638b
commit 855976e2b6
+202 -54
View File
@@ -797,9 +797,10 @@
MAP STYLES
═══════════════════════════════════════════════════════════════════════ */
#mapContainer {
height: calc(100dvh - var(--header-height) - var(--tab-height) - 60px);
min-height: 300px;
width: 100%;
aspect-ratio: 1 / 1;
max-height: 60vh;
min-height: 300px;
border-radius: var(--radius);
overflow: hidden;
border: 1px solid var(--border);
@@ -813,6 +814,7 @@
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
}
/* Map controls bar */
.map-controls {
display: flex; gap: 6px; margin-bottom: 6px; flex-wrap: wrap;
@@ -827,6 +829,49 @@
.map-chip.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.map-chip.heat-on { background: var(--amber); border-color: var(--amber); color: #000; }
.map-chip.park-on { background: var(--accent); border-color: var(--accent); color: #fff; }
/* Map search bar */
.map-search-row {
display: flex; gap: 6px; margin-bottom: 6px; align-items: center;
}
.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 {
margin-top: 8px;
max-height: 280px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 4px;
}
.map-asset-item {
display: flex; align-items: center; gap: 8px;
padding: 8px 10px; border-radius: var(--radius-xs);
background: var(--card); border: 1px solid var(--border);
cursor: pointer; transition: background 0.12s;
font-size: 13px;
}
.map-asset-item:hover { background: var(--card2); }
.map-asset-item .mai-icon { font-size: 16px; width: 24px; text-align: center; }
.map-asset-item .mai-name { flex: 1; font-weight: 600; color: var(--text); }
.map-asset-item .mai-meta { font-size: 11px; color: var(--text3); }
.map-asset-item .mai-dist { font-size: 11px; color: var(--accent2); white-space: nowrap; }
.map-asset-empty {
text-align: center; padding: 24px; color: var(--text3); font-size: 13px;
}
.cat-pin-icon { background: transparent !important; border: none !important; box-shadow: none !important; }
.park-label-icon { pointer-events: none !important; background: transparent !important; border: none !important; }
.leaflet-popup-content-wrapper {
@@ -1535,7 +1580,18 @@
<span class="map-chip park-on" id="chipParkOutlines" onclick="toggleParkOutlines()">🏰 Parks ON</span>
<span class="map-chip" id="chipGeoGPS" onclick="if(navigator.geolocation)navigator.geolocation.getCurrentPosition(p=>{map.setView([p.coords.latitude,p.coords.longitude],15)})">◎ My GPS</span>
</div>
<!-- Search bar -->
<div class="map-search-row">
<input type="text" id="mapSearchInput" class="search-input" placeholder="Search by name, machine ID, category..."
oninput="mapDebounceFilter()">
<button class="btn btn-secondary btn-sm" onclick="document.getElementById('mapSearchInput').value='';mapDebounceFilter()" style="white-space:nowrap;"></button>
</div>
<!-- Filter chips -->
<div class="map-filter-chips" id="mapFilterChips"></div>
<!-- Map -->
<div id="mapContainer"></div>
<!-- Asset list -->
<div id="mapAssetList" class="map-asset-list"></div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════════
@@ -3005,7 +3061,7 @@
}
// If GPS is missing, warn but still let them check in
if (AppState.gpsLat == null) {
if (!confirm('No GPS location captured. Check in without location data? Press Cancel to go back and tap "Capture GPS" first.')) {
if (!(await showModal('No GPS', 'No GPS location captured. Check in without location data?', 'Check In', 'btn-primary'))) {
return;
}
}
@@ -4859,9 +4915,14 @@
}
// ═══════════════════════════════════════════════════════════════════════
// MAP PINS
// MAP PINS + FILTER + SEARCH + ASSET LIST
// ═══════════════════════════════════════════════════════════════════════
var _mapAllAssets = []; // All assets from API (cached)
var _mapFilterTimer = null; // Debounce timer
var _mapActiveCategory = ''; // Active category filter chip
var _mapActiveStatus = ''; // Active status filter chip
async function loadAssetPins() {
// Store markers on a namespace so we can clear them
if (!window._assetPins) window._assetPins = [];
@@ -4870,42 +4931,138 @@
window._assetPins = [];
try {
const assets = await api('/api/assets?limit=1000');
assets.forEach(function(a) {
if (a.latitude != null && a.longitude != null) {
var catEntry = CATEGORY_MAP[a.category];
var icon = catEntry ? catEntry.icon : (a.category ? a.category.charAt(0).toUpperCase() : '📦');
// Map pins can't render <img> tags, fall back to emoji
if (icon.indexOf('<img') !== -1) icon = '🍿';
var popupHtml = '<b>' + esc(a.name) + '</b><br>' + esc(a.machine_id || '') +
'<br><button class="btn btn-outline btn-sm" style="margin-top:6px;" ' +
'onclick="switchTab(\'tabAssets\');viewAsset(' + a.id + ');">View Details</button>';
var catColor = catEntry ? catEntry.color : '#6b7280';
var pinIcon = L.divIcon({
className: 'cat-pin-icon',
html: '<div style="width:28px;height:28px;border-radius:50%;background:' + catColor +
';display:flex;align-items:center;justify-content:center;font-size:15px;' +
'box-shadow:0 2px 5px rgba(0,0,0,0.5);border:2px solid #fff;">' + icon + '</div>',
iconSize: [32, 32],
iconAnchor: [16, 16],
popupAnchor: [0, -18],
});
var marker = L.marker([a.latitude, a.longitude], { icon: pinIcon })
.addTo(map)
.bindPopup(popupHtml);
window._assetPins.push(marker);
}
});
// If we have assets with coords, zoom to fit them
if (window._assetPins.length > 0) {
var group = L.featureGroup(window._assetPins);
map.fitBounds(group.getBounds().pad(0.1));
}
const assets = await api('/api/assets?limit=2000');
_mapAllAssets = assets;
buildMapFilterChips(assets);
// Apply current filter
applyMapFilter();
} catch (e) {
console.error('Failed to load asset pins:', e);
}
}
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 += '<span class="map-filter-chip' + (!_mapActiveCategory ? ' active' : '') + '" onclick="mapSetCategory(\'\')">All</span>';
catKeys.forEach(function(c) {
html += '<span class="map-filter-chip' + (_mapActiveCategory === c ? ' active' : '') + '" onclick="mapSetCategory(\'' + c.replace(/'/g, "\\'") + '\')">' + esc(c) + ' (' + cats[c] + ')</span>';
});
html += '<span class="map-filter-chip' + (!_mapActiveStatus ? ' active' : '') + '" onclick="mapSetStatus(\'\')">All Status</span>';
statusKeys.forEach(function(s) {
html += '<span class="map-filter-chip' + (_mapActiveStatus === s ? ' active' : '') + '" onclick="mapSetStatus(\'' + s.replace(/'/g, "\\'") + '\')">' + esc(s) + '</span>';
});
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);
_mapFilterTimer = setTimeout(applyMapFilter, 200);
}
function applyMapFilter() {
if (_mapFilterTimer) { clearTimeout(_mapFilterTimer); _mapFilterTimer = null; }
var q = (document.getElementById('mapSearchInput').value || '').toLowerCase().trim();
// Clear existing pins
window._assetPins.forEach(function(m) { if (map) map.removeLayer(m); });
window._assetPins = [];
var matched = [];
_mapAllAssets.forEach(function(a) {
// Category filter
if (_mapActiveCategory && a.category !== _mapActiveCategory) return;
// Status filter
if (_mapActiveStatus && a.status !== _mapActiveStatus) return;
// Text search
if (q) {
var haystack = ((a.name || '') + ' ' + (a.machine_id || '') + ' ' + (a.category || '') + ' ' + (a.description || '')).toLowerCase();
if (haystack.indexOf(q) === -1) return;
}
matched.push(a);
});
// Render pins for assets with GPS
matched.forEach(function(a) {
if (a.latitude != null && a.longitude != null) {
var catEntry = CATEGORY_MAP[a.category];
var icon = catEntry ? catEntry.icon : (a.category ? a.category.charAt(0).toUpperCase() : '📦');
if (icon.indexOf('<img') !== -1) icon = '🍽';
var popupHtml = '<b>' + esc(a.name) + '</b><br>' + esc(a.machine_id || '') +
'<br><button class="btn btn-outline btn-sm" style="margin-top:6px;" ' +
'onclick="switchTab(\'tabAssets\');viewAsset(' + a.id + ');">View Details</button>';
var catColor = catEntry ? catEntry.color : '#6b7280';
var pinIcon = L.divIcon({
className: 'cat-pin-icon',
html: '<div style="width:28px;height:28px;border-radius:50%;background:' + catColor +
';display:flex;align-items:center;justify-content:center;font-size:15px;' +
'box-shadow:0 2px 5px rgba(0,0,0,0.5);border:2px solid #fff;">' + icon + '</div>',
iconSize: [32, 32],
iconAnchor: [16, 16],
popupAnchor: [0, -18],
});
var marker = L.marker([a.latitude, a.longitude], { icon: pinIcon })
.addTo(map)
.bindPopup(popupHtml);
window._assetPins.push(marker);
}
});
// Fit bounds to visible pins
if (window._assetPins.length > 0) {
var group = L.featureGroup(window._assetPins);
map.fitBounds(group.getBounds().pad(0.1));
}
// Render asset list below map
renderMapAssetList(matched);
}
function renderMapAssetList(assets) {
var el = document.getElementById('mapAssetList');
if (assets.length === 0) {
el.innerHTML = '<div class="map-asset-empty">📦 No assets match your search</div>';
return;
}
// Sort by name
var sorted = assets.slice().sort(function(a, b) { return (a.name || '').localeCompare(b.name || ''); });
var html = '';
sorted.forEach(function(a) {
var catEntry = CATEGORY_MAP[a.category];
var icon = catEntry ? catEntry.icon : '📦';
if (icon.indexOf('<img') !== -1) icon = '🍽';
var hasGps = (a.latitude != null && a.longitude != null);
html += '<div class="map-asset-item" onclick="switchTab(\'tabAssets\');viewAsset(' + a.id + ');">' +
'<span class="mai-icon">' + icon + '</span>' +
'<span class="mai-name">' + esc(a.name) + '</span>' +
'<span class="mai-meta">' + esc(a.machine_id || '') + (hasGps ? ' · 📍' : '') + '</span>' +
'<span class="mai-dist">' + esc(a.category || '') + '</span>' +
'</div>';
});
el.innerHTML = html;
}
// ═══════════════════════════════════════════════════════════════════════
// HEATMAP
// ═══════════════════════════════════════════════════════════════════════
@@ -4939,7 +5096,7 @@
});
// Get all assets with coordinates
const assets = await api('/api/assets?limit=1000');
const assets = await api('/api/assets?limit=2000');
const heatData = [];
assets.forEach(a => {
if (a.latitude != null && a.longitude != null) {
@@ -5230,10 +5387,10 @@
}
function rpUseMyLocation() {
if (!navigator.geolocation) return alert('Geolocation not available');
if (!navigator.geolocation) { showToast('Geolocation not available', true); return; }
navigator.geolocation.getCurrentPosition(
pos => rpSetOrigin(pos.coords.latitude, pos.coords.longitude),
() => alert('Could not get location')
() => showToast('Could not get location', true)
);
}
@@ -5323,7 +5480,7 @@
async function rpLoadToday() {
const techs = [];
document.querySelectorAll('#rpTechCheckboxes input[type="checkbox"]:not(#rpTechAll):checked').forEach(cb => techs.push(cb.value));
if (techs.length === 0) { return alert('Select at least one technician'); }
if (techs.length === 0) { showToast('Select at least one technician', true); return; }
rpShowLoading('Loading today\u2019s work orders...');
try {
@@ -5331,22 +5488,22 @@
const data = await api('/api/workorders/today?' + params);
if (!data.workorders || data.workorders.length === 0) {
rpHideLoading();
return alert('No work orders found for today');
showToast('No work orders found for today', true); return;
}
const woIds = data.workorders.map(wo => wo.name);
await rpOptimizeRoute(woIds, data.workorders);
} catch (e) {
rpHideLoading();
alert('Error loading today\u2019s route: ' + e.message);
showToast('Error loading today\'s route: ' + e.message, true);
}
}
// ── Plan from Paste ───────────────────────────────────────────────────
function rpPlanFromPaste() {
const raw = document.getElementById('rpWoInput').value.trim();
if (!raw) return alert('Paste some work order numbers first');
if (!raw) { showToast('Paste some work order numbers first', true); return; }
const ids = raw.split(/[,\n\r]+/).map(s => s.trim()).filter(Boolean);
if (ids.length === 0) return alert('No valid work order numbers found');
if (ids.length === 0) { showToast('No valid work order numbers found', true); return; }
rpOptimizeRoute(ids);
}
@@ -5373,7 +5530,7 @@
woDetails = data.found || [];
} catch (e) {
rpHideLoading();
return alert('Error looking up work orders: ' + e.message);
showToast('Error looking up work orders: ' + e.message, true); return;
}
}
@@ -5393,7 +5550,7 @@
rpDisplayRoute(data, woDetails);
} catch (e) {
rpHideLoading();
alert('Error optimizing route: ' + e.message);
showToast('Error optimizing route: ' + e.message, true);
}
}
@@ -5613,15 +5770,6 @@
}
}
// ── Helper ────────────────────────────────────────────────────────────
function esc(s) {
if (!s) return '';
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
document.addEventListener('DOMContentLoaded', () => {
initAuth();
initGPS();