From cce5cf2b5fec11d861ef8c87430cd7326e461487 Mon Sep 17 00:00:00 2001 From: Shawn Date: Fri, 22 May 2026 17:53:32 -0400 Subject: [PATCH] Add Disney park badges, filter pills, editable classification, and service entrances to asset detail view --- static/index.html | 150 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 139 insertions(+), 11 deletions(-) diff --git a/static/index.html b/static/index.html index 6eca304..8b76d19 100644 --- a/static/index.html +++ b/static/index.html @@ -221,6 +221,12 @@ letter-spacing: 0.05em; color: var(--text2); margin-bottom: 10px; } + .dropdown-item { + padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 13px; + transition: background .15s; + } + .dropdown-item:hover { background: var(--hover); } + /* ═══════════════════════════════════════════════════════════════════════ BUTTONS ═══════════════════════════════════════════════════════════════════════ */ @@ -416,6 +422,11 @@ .status-tag.maintenance { background: var(--amber-bg); color: var(--amber); } .status-tag.retired { background: var(--red-bg); color: var(--red); } + .disney-tag { + display: inline-block; font-size: 10px; padding: 2px 8px; border-radius: 4px; + font-weight: 600; background: #8b5cf622; color: #8b5cf6; + } + /* ═══════════════════════════════════════════════════════════════════════ SEARCH BAR ═══════════════════════════════════════════════════════════════════════ */ @@ -1089,6 +1100,12 @@
+ + +
Check-in History
@@ -1322,6 +1339,30 @@ function isLoggedIn() { return !!AppState.authToken; } function isTechnician() { return AppState.currentUser && AppState.currentUser.role === 'technician'; } + // ── Dropdown helper ──────────────────────────────────────────────────── + let _activeDropdown = null; + function showDropdown(anchorEl, itemsHtml) { + closeDropdown(); + const rect = anchorEl.getBoundingClientRect(); + const el = document.createElement('div'); + el.id = '__dropdown'; + el.style.cssText = 'position:fixed;z-index:9999;background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:6px;box-shadow:0 8px 24px rgba(0,0,0,.4);min-width:200px'; + el.style.top = (rect.bottom + 6) + 'px'; + el.style.left = Math.min(rect.left, window.innerWidth - 220) + 'px'; + el.innerHTML = itemsHtml; + document.body.appendChild(el); + _activeDropdown = el; + // Click outside to close + setTimeout(() => document.addEventListener('click', _closeDropdownHandler), 10); + } + function _closeDropdownHandler() { closeDropdown(); document.removeEventListener('click', _closeDropdownHandler); } + function closeDropdown() { + if (_activeDropdown) { + _activeDropdown.remove(); + _activeDropdown = null; + } + } + function updateUserUI() { const u = AppState.currentUser; const initial = u ? u.username.charAt(0).toUpperCase() : '?'; @@ -2966,8 +3007,13 @@ return '📦'; } + const PARK_ICONS = {'magic-kingdom':'🏰','epcot':'🌍','hollywood-studios':'🎬','animal-kingdom':'🌿','disney-springs':'🛍️','resort':'🏨','office':'🏢','other':'📍'}; + const PARK_NAMES = {'magic-kingdom':'Magic Kingdom','epcot':'Epcot','hollywood-studios':'Hollywood Studios','animal-kingdom':'Animal Kingdom','disney-springs':'Disney Springs','resort':'Resort','office':'Office','other':'Other'}; + function parkIcon(key) { return PARK_ICONS[key] || '📍'; } + function parkName(key) { return PARK_NAMES[key] || key; } + // ── Filter state ────────────────────────────────────────────────────── - let assetFilters = { category: null, status: null, make: null, q: '' }; + let assetFilters = { category: null, status: null, make: null, disney_park: null, q: '' }; let assetDebounce = null; let cachedAssets = []; @@ -3004,6 +3050,7 @@ var params = new URLSearchParams(); if (assetFilters.category) params.set('category', assetFilters.category); if (assetFilters.status) params.set('status', assetFilters.status); + if (assetFilters.make) params.set('make', assetFilters.make); if (assetFilters.q) params.set('q', assetFilters.q); if (assetFilters.assigned_to) params.set('assigned_to', assetFilters.assigned_to); params.set('limit', '100'); @@ -3014,6 +3061,9 @@ var filtered = assetFilters.assigned_to ? assets.filter(function(a) { return a.assigned_to === assetFilters.assigned_to; }) : assets; + filtered = assetFilters.disney_park + ? filtered.filter(function(a) { return a.disney_park === assetFilters.disney_park; }) + : filtered; renderAssetList(filtered); renderFilterPills(assets); } catch (e) { @@ -3037,9 +3087,10 @@
${esc(a.name)}
- ${esc(a.machine_id)}${a.serial_number ? ' · S/N: ' + esc(a.serial_number) : ''} - ${a.make ? ' · ' + esc(a.make) : ''} - · ${a.status || 'active'} + ${esc(a.machine_id)}${a.serial_number ? ' · S/N: ' + esc(a.serial_number) : ''} + ${a.make ? ' · ' + esc(a.make) : ''} + ${a.disney_park ? ' · ' + parkIcon(a.disney_park) + ' ' + parkName(a.disney_park) + '' : ''} + · ${a.status || 'active'}
@@ -3048,37 +3099,45 @@ } function renderFilterPills(assets) { - const cats = new Set(), stats = new Set(), makes = new Set(); + const cats = new Set(), stats = new Set(), makes = new Set(), parks = new Set(); assets.forEach(a => { if (a.category) cats.add(a.category); if (a.status) stats.add(a.status); if (a.make) makes.add(a.make); + if (a.disney_park) parks.add(a.disney_park); }); - let html = 'All'; + let html = 'All'; // Category pills [...cats].sort().forEach(c => { - html += `📂 ${esc(c)}`; + html += `📂 ${esc(c)}`; }); // Status pills [...stats].sort().forEach(s => { - html += `${s === 'active' ? '🟢' : s === 'maintenance' ? '🟡' : '🔴'} ${s}`; + html += `${s === 'active' ? '🟢' : s === 'maintenance' ? '🟡' : '🔴'} ${s}`; }); // Make pills [...makes].sort().forEach(m => { - html += `🏭 ${esc(m)}`; + html += `🏭 ${esc(m)}`; + }); + + // Disney Park pills + const sortedParks = [...parks].sort((a,b) => (PARK_NAMES[a]||a).localeCompare(PARK_NAMES[b]||b)); + sortedParks.forEach(p => { + html += `${parkIcon(p)} ${parkName(p)}`; }); document.getElementById('filterPills').innerHTML = html; } - function setFilter(category, status, make) { + function setFilter(category, status, make, disney_park) { assetFilters.category = category; assetFilters.status = status; assetFilters.make = make; + assetFilters.disney_park = disney_park; loadAssets(); } @@ -3132,7 +3191,7 @@ document.getElementById('detailName').textContent = a.name; document.getElementById('detailMeta').innerHTML = - `${esc(a.machine_id)} · ${a.status || 'active'}`; + `${esc(a.machine_id)} · ${a.status || 'active'}${a.disney_park ? ' · ' + parkIcon(a.disney_park) + ' ' + parkName(a.disney_park) + '' : ''}`; // Main details document.getElementById('detailFields').innerHTML = [ @@ -3146,6 +3205,21 @@ df('Status', statusBadge(a.status)), ].filter(Boolean).join(''); + // Disney Park badge + if (a.disney_park) { + const parkIcons = {'magic-kingdom':'🏰','epcot':'🌍','hollywood-studios':'🎬','animal-kingdom':'🌿','disney-springs':'🛍️','resort':'🏨','office':'🏢','other':'📍'}; + const parkNames = {'magic-kingdom':'Magic Kingdom','epcot':'Epcot','hollywood-studios':'Hollywood Studios','animal-kingdom':'Animal Kingdom','disney-springs':'Disney Springs','resort':'Resort','office':'Office','other':'Other'}; + const parkColors = {'magic-kingdom':'#8b5cf6','epcot':'#06b6d4','hollywood-studios':'#f59e0b','animal-kingdom':'#22c55e','disney-springs':'#ec4899','resort':'#3b82f6','office':'#6b7280','other':'#95a5a6'}; + const icon = parkIcons[a.disney_park] || '📍'; + const name = parkNames[a.disney_park] || a.disney_park; + const color = parkColors[a.disney_park] || '#95a5a6'; + document.getElementById('detailFields').innerHTML += + '
' + + '
Disney Park
' + + '' + + icon + ' ' + esc(name) + ' ✏️
'; + } + // Customer / Location / Technician if (a.customer_name) { document.getElementById('detailFields').innerHTML += df('Customer', @@ -3221,6 +3295,9 @@ accessCard.style.display = 'none'; } + // Service Entrances + await loadServiceEntrances(a); + await loadCheckinHistory(id); } catch (e) { showToast(e.message, true); @@ -3431,6 +3508,57 @@ } } + async function loadServiceEntrances(a) { + const el = document.getElementById('detailServiceEntrances'); + const fields = document.getElementById('detailSEFields'); + if (!a.location_id) { + el.style.display = 'none'; + return; + } + try { + const ses = await api('/api/locations/' + a.location_id + '/service-entrances'); + if (!ses || !ses.length) { + el.style.display = 'none'; + return; + } + el.style.display = 'block'; + fields.innerHTML = ses.map(se => ` +
+
${esc(se.name || 'Service Entrance')}
+
+ 📍 ${Number(se.latitude).toFixed(5)}, ${Number(se.longitude).toFixed(5)} + ${se.notes ? '
📝 ' + esc(se.notes) : ''} +
+
+ `).join(''); + } catch (e) { + el.style.display = 'none'; + } + } + + async function changeDisneyPark(assetId) { + const parks = ['magic-kingdom','epcot','hollywood-studios','animal-kingdom','disney-springs','resort','office','other']; + const current = document.getElementById('disneyParkBadge')?.textContent?.trim() || ''; + const options = parks.map(p => '').join(''); + const noneOpt = ''; + showDropdown(document.getElementById('disneyParkBadge'), options + noneOpt); + } + + async function setDisneyPark(assetId, park) { + closeDropdown(); + try { + await api('/api/assets/' + assetId + '/disney-park', { + method: 'PUT', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({disney_park: park}) + }); + await viewAsset(assetId); + showToast(park ? 'Set to ' + parkName(park) : 'Disney park cleared'); + } catch (e) { + showToast(e.message, true); + } + } + // ── Edit ────────────────────────────────────────────────────────────── async function editAsset() { if (!AppState.currentAssetId) return;