Add disney_group grouped park filter categories

- New disney_group query param on /api/assets (park, resort,
  office, springs, other)
- Both list and map filter dropdowns show group options above
  individual parks
- Backend uses IN queries for grouped park values
- Map view handles groups via client-side PARK_GROUP_MAP

Closes #69
This commit is contained in:
2026-06-03 00:57:55 -04:00
parent c7ebef7cc1
commit 874e15c01e
2 changed files with 89 additions and 13 deletions
+14
View File
@@ -1065,6 +1065,7 @@ def list_assets(
assigned_to: Optional[int] = Query(None),
disney_park: Optional[str] = Query(None),
disney_filter: Optional[str] = Query(None),
disney_group: Optional[str] = Query(None, description="Grouped park categories: park, resort, office, springs, other"),
q: Optional[str] = Query(None),
no_dex_days: Optional[int] = Query(None, ge=1),
has_gps: Optional[bool] = Query(None),
@@ -1092,6 +1093,19 @@ def list_assets(
conditions.append("is_disney = 1")
elif disney_filter == "non_disney":
conditions.append("(is_disney IS NULL OR is_disney = 0)")
if disney_group:
park_groups = {
"park": ["magic-kingdom", "epcot", "hollywood-studios", "animal-kingdom"],
"resort": ["resort"],
"office": ["office"],
"springs": ["disney-springs"],
"other": ["other"],
}
parks = park_groups.get(disney_group)
if parks:
placeholders = ",".join("?" * len(parks))
conditions.append(f"disney_park IN ({placeholders})")
params.extend(parks)
if model:
conditions.append("model = ?")
params.append(model)
+75 -13
View File
@@ -1340,7 +1340,13 @@
<div class="fp-label">Disney Park</div>
<select id="filterDisneyPark" class="input-field" onchange="onFilterChange()">
<option value="">All parks</option>
<option disabled>──────────</option>
<option disabled>──── Groups ────</option>
<option value="group:park">🏰 Disney Park (all parks)</option>
<option value="group:resort">🏨 Disney Resort</option>
<option value="group:office">🏢 Disney Office</option>
<option value="group:springs">🛍️ Disney Springs</option>
<option value="group:other">📍 Other Disney</option>
<option disabled>── Individual ──</option>
<option value="magic-kingdom">🏰 Magic Kingdom</option>
<option value="epcot">🌍 Epcot</option>
<option value="hollywood-studios">🎬 Hollywood Studios</option>
@@ -1550,7 +1556,13 @@
<div class="fp-label">Disney Park</div>
<select id="mapFilterDisneyPark" class="input-field" onchange="onMapFilterChange()">
<option value="">All parks</option>
<option disabled>──────────</option>
<option disabled>──── Groups ────</option>
<option value="group:park">🏰 Disney Park (all parks)</option>
<option value="group:resort">🏨 Disney Resort</option>
<option value="group:office">🏢 Disney Office</option>
<option value="group:springs">🛍️ Disney Springs</option>
<option value="group:other">📍 Other Disney</option>
<option disabled>── Individual ──</option>
<option value="magic-kingdom">🏰 Magic Kingdom</option>
<option value="epcot">🌍 Epcot</option>
<option value="hollywood-studios">🎬 Hollywood Studios</option>
@@ -3899,6 +3911,15 @@
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'};
const PARK_GROUP_ICONS = {'park':'🏰','resort':'🏨','office':'🏢','springs':'🛍️','other':'📍'};
const PARK_GROUP_NAMES = {'park':'Disney Park','resort':'Disney Resort','office':'Disney Office','springs':'Disney Springs','other':'Other Disney'};
const PARK_GROUP_MAP = {
'park': ['magic-kingdom','epcot','hollywood-studios','animal-kingdom'],
'resort': ['resort'],
'office': ['office'],
'springs': ['disney-springs'],
'other': ['other'],
};
function catLabel(cat) {
const e = CATEGORY_MAP[cat];
return e && e.label ? e.label : (cat || 'Other');
@@ -4124,7 +4145,7 @@
}
// ── Filter state ──────────────────────────────────────────────────────
let assetFilters = { category: null, status: null, make: null, disney_park: null, disney_filter: null, q: '',
let assetFilters = { category: null, status: null, make: null, disney_park: null, disney_group: null, disney_filter: null, q: '',
customer_id: null, location_id: null, assigned_to: null, branch: null };
let noDexFilterActive = false;
let assetOffset = 0;
@@ -4222,10 +4243,23 @@
if (disneyFilterVal) {
assetFilters.disney_filter = disneyFilterVal;
assetFilters.disney_park = null;
assetFilters.disney_group = null;
} else {
var parkVal = parkEl.value;
assetFilters.disney_park = parkVal || null;
assetFilters.disney_filter = null;
if (parkVal && parkVal.startsWith('group:')) {
// Grouped park category
assetFilters.disney_group = parkVal.replace('group:', '');
assetFilters.disney_park = null;
assetFilters.disney_filter = null;
} else if (parkVal) {
assetFilters.disney_park = parkVal;
assetFilters.disney_group = null;
assetFilters.disney_filter = null;
} else {
assetFilters.disney_park = null;
assetFilters.disney_group = null;
assetFilters.disney_filter = null;
}
}
assetFilters.customer_id = custEl.value || null;
@@ -4266,6 +4300,7 @@
if (assetFilters.make) count++;
if (assetFilters.disney_park) count++;
if (assetFilters.disney_group) count++;
if (assetFilters.disney_filter) count++;
if (assetFilters.customer_id) count++;
if (assetFilters.location_id) count++;
@@ -4292,6 +4327,7 @@
if (assetFilters.make) tags.push({key:'make', label:'\uD83C\uDFED ' + assetFilters.make});
if (assetFilters.disney_park) tags.push({key:'disney_park', label: parkIcon(assetFilters.disney_park) + ' ' + parkName(assetFilters.disney_park)});
if (assetFilters.disney_group) tags.push({key:'disney_group', label: (PARK_GROUP_ICONS[assetFilters.disney_group] || '📍') + ' ' + (PARK_GROUP_NAMES[assetFilters.disney_group] || assetFilters.disney_group)});
if (assetFilters.disney_filter === 'disney') tags.push({key:'disney_filter', label:'🏰 Disney only'});
if (assetFilters.disney_filter === 'non_disney') tags.push({key:'disney_filter', label:'🏢 Non-Disney'});
if (assetFilters.customer_id) {
@@ -4334,6 +4370,9 @@
} else if (key === 'disney_park') {
document.getElementById('filterDisneyPark').value = '';
assetFilters.disney_park = null;
} else if (key === 'disney_group') {
document.getElementById('filterDisneyPark').value = '';
assetFilters.disney_group = null;
} else if (key === 'disney_filter') {
document.getElementById('filterDisneyFilter').value = '';
assetFilters.disney_filter = null;
@@ -4363,7 +4402,7 @@
}
function clearAllFilters() {
assetFilters = { category: null, status: null, make: null, disney_park: null, disney_filter: null, q: '',
assetFilters = { category: null, status: null, make: null, disney_park: null, disney_group: null, disney_filter: null, q: '',
customer_id: null, location_id: null, assigned_to: null, branch: null };
document.getElementById('assetSearch').value = '';
document.getElementById('clearSearch').style.display = 'none';
@@ -5001,6 +5040,7 @@
if (assetFilters.make) params.set('make', assetFilters.make);
if (assetFilters.disney_park) params.set('disney_park', assetFilters.disney_park);
if (assetFilters.disney_group) params.set('disney_group', assetFilters.disney_group);
if (assetFilters.disney_filter) params.set('disney_filter', assetFilters.disney_filter);
if (assetFilters.q) params.set('q', assetFilters.q);
if (assetFilters.customer_id) params.set('customer_id', assetFilters.customer_id);
@@ -5069,16 +5109,17 @@
return parts.join('');
}
function setFilter(category, status, make, disney_park) {
function setFilter(category, status, make, disney_park, disney_group) {
assetFilters.category = category;
assetFilters.status = status;
assetFilters.make = make;
assetFilters.disney_park = disney_park;
assetFilters.disney_group = disney_group || null;
assetFilters.disney_filter = null;
// Sync dropdowns
if (document.getElementById('filterCategory')) document.getElementById('filterCategory').value = category || '';
if (document.getElementById('filterMake')) document.getElementById('filterMake').value = make || '';
if (document.getElementById('filterDisneyPark')) document.getElementById('filterDisneyPark').value = disney_park || '';
if (document.getElementById('filterDisneyPark')) document.getElementById('filterDisneyPark').value = (disney_group ? 'group:' + disney_group : (disney_park || ''));
renderActiveFilterTags();
assetOffset = 0;
@@ -6069,7 +6110,7 @@
}
// ── Map filter state ──────────────────────────────────────────────────
let mapFilters = { category: null, status: null, make: null, disney_park: null, disney_filter: null, q: '',
let mapFilters = { category: null, status: null, make: null, disney_park: null, disney_group: null, disney_filter: null, q: '',
customer_id: null, location_id: null, assigned_to: null, branch: null };
let mapFilterDropdownsLoaded = false;
let _mapFilterTimer;
@@ -6128,10 +6169,22 @@
if (disneyFilterVal) {
mapFilters.disney_filter = disneyFilterVal;
mapFilters.disney_park = null;
mapFilters.disney_group = null;
} else {
var parkVal = document.getElementById('mapFilterDisneyPark').value;
mapFilters.disney_park = parkVal || null;
mapFilters.disney_filter = null;
if (parkVal && parkVal.startsWith('group:')) {
mapFilters.disney_group = parkVal.replace('group:', '');
mapFilters.disney_park = null;
mapFilters.disney_filter = null;
} else if (parkVal) {
mapFilters.disney_park = parkVal;
mapFilters.disney_group = null;
mapFilters.disney_filter = null;
} else {
mapFilters.disney_park = null;
mapFilters.disney_group = null;
mapFilters.disney_filter = null;
}
}
mapFilters.customer_id = document.getElementById('mapFilterCustomer').value || null;
@@ -6172,6 +6225,7 @@
if (mapFilters.category) count++;
if (mapFilters.make) count++;
if (mapFilters.disney_park) count++;
if (mapFilters.disney_group) count++;
if (mapFilters.disney_filter) count++;
if (mapFilters.customer_id) count++;
if (mapFilters.location_id) count++;
@@ -6196,6 +6250,7 @@
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_group) tags.push({key:'disney_group', label: (PARK_GROUP_ICONS[mapFilters.disney_group] || '📍') + ' ' + (PARK_GROUP_NAMES[mapFilters.disney_group] || mapFilters.disney_group)});
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) {
@@ -6229,6 +6284,9 @@
} else if (key === 'disney_park') {
document.getElementById('mapFilterDisneyPark').value = '';
mapFilters.disney_park = null;
} else if (key === 'disney_group') {
document.getElementById('mapFilterDisneyPark').value = '';
mapFilters.disney_group = null;
} else if (key === 'disney_filter') {
document.getElementById('mapFilterDisneyFilter').value = '';
mapFilters.disney_filter = null;
@@ -6253,7 +6311,7 @@
}
function clearMapFilters() {
mapFilters = { category: null, status: null, make: null, disney_park: null, disney_filter: null, q: '',
mapFilters = { category: null, status: null, make: null, disney_park: null, disney_group: null, disney_filter: null, q: '',
customer_id: null, location_id: null, assigned_to: null, branch: null };
document.getElementById('mapSearchInput').value = '';
document.getElementById('mapFilterCategory').value = '';
@@ -6310,7 +6368,11 @@
// 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
// Disney park filter (individual or group)
if (mapFilters.disney_group) {
var groupParks = PARK_GROUP_MAP[mapFilters.disney_group] || [];
if (groupParks.indexOf(a.disney_park) === -1) return;
}
if (mapFilters.disney_park && a.disney_park !== mapFilters.disney_park) return;
// Text search
if (q) {