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:
+202
-54
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user