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 => '
' + parkIcon(p) + ' ' + parkName(p) + (current.includes(parkName(p)) ? ' ✓' : '') + '
').join('');
+ const noneOpt = '
❌ Clear
';
+ 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;