diff --git a/server.py b/server.py index b56c624..7b109f1 100644 --- a/server.py +++ b/server.py @@ -930,6 +930,16 @@ def list_makes(): return [r["manufacturer"] for r in rows] +@app.get("/api/places") +def list_places(): + conn = get_db() + rows = conn.execute( + "SELECT DISTINCT place FROM assets WHERE place IS NOT NULL AND place != '' ORDER BY place" + ).fetchall() + conn.close() + return [r["place"] for r in rows] + + # ─── Task 4: POST /api/assets ────────────────────────────────────────────── @@ -1067,6 +1077,8 @@ def list_assets( 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), + place: Optional[str] = Query(None, description="Filter by place/city"), + location_text: Optional[str] = Query(None, description="Free-text location filter (building, facility)"), no_dex_days: Optional[int] = Query(None, ge=1), has_gps: Optional[bool] = Query(None), branch: Optional[str] = Query(None, description="Filter by branch/territory name"), @@ -1122,6 +1134,13 @@ def list_assets( conditions.append("(a.name LIKE ? OR a.machine_id LIKE ? OR a.serial_number LIKE ? OR a.description LIKE ? OR a.company LIKE ? OR a.place LIKE ? OR a.customer_name LIKE ? OR a.address LIKE ?)") like = f"%{q}%" params.extend([like, like, like, like, like, like, like, like]) + if place: + conditions.append("a.place = ?") + params.append(place) + if location_text: + conditions.append("(a.building_name LIKE ? OR a.address LIKE ? OR a.location_area LIKE ? OR a.place LIKE ? OR a.company LIKE ?)") + lt = f"%{location_text}%" + params.extend([lt, lt, lt, lt, lt]) if no_dex_days is not None: conditions.append("(dex_report_date IS NULL OR REPLACE(dex_report_date, 'T', ' ') < datetime('now', '-' || ? || ' days'))") params.append(no_dex_days) @@ -3659,6 +3678,7 @@ async def workorders_list( w."msdyn_workordertype!name" AS work_type, w."msdyn_primaryincidenttype!name" AS incident_type, w."msdyn_serviceterritory!name" AS territory, + w."msdyn_functionallocation!name" AS functional_location, w.msdyn_instructions, w.hsl_onsiteduration, w.msdyn_address1, @@ -3720,6 +3740,7 @@ async def workorders_list( "incident_type": r["incident_type"], "territory": r["territory"], "customer_asset_name": r["customer_asset_name"], + "functional_location": r["functional_location"], "instructions": r["msdyn_instructions"], "onsite_duration": r["hsl_onsiteduration"], "datewindow_start": r["msdyn_datewindowstart"], diff --git a/static/index.html b/static/index.html index ca7155d..93ea5b5 100644 --- a/static/index.html +++ b/static/index.html @@ -1367,11 +1367,15 @@
-
Location
- +
+
+
Location Facility
+ +
@@ -1878,6 +1882,12 @@
+
+
+
Location / Facility
+ +
+
Status
@@ -4153,7 +4163,7 @@ // ── Filter state ────────────────────────────────────────────────────── 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 }; + customer_id: null, location_id: null, assigned_to: null, branch: null, place: null, location_text: null }; let noDexFilterActive = false; let assetOffset = 0; const ASSET_PAGE_SIZE = 2000; @@ -4196,6 +4206,15 @@ var custSel = document.getElementById('filterCustomer'); custSel.innerHTML = '' + customers.map(function(c) { return ''; }).join(''); + + // Load places + try { + var places = await api('/api/places'); + var placeSel = document.getElementById('filterPlace'); + if (placeSel) placeSel.innerHTML = '' + + places.map(function(p) { return ''; }).join(''); + } catch(e) {} + // Load users for assigned_to filter var users = await api('/api/users'); var assnSel = document.getElementById('filterAssignedTo'); @@ -4238,7 +4257,8 @@ var disneyFilterEl = document.getElementById('filterDisneyFilter'); var parkEl = document.getElementById('filterDisneyPark'); var custEl = document.getElementById('filterCustomer'); - var locEl = document.getElementById('filterLocation'); + var placeEl = document.getElementById('filterPlace'); + var locTextEl = document.getElementById('filterLocationText'); var assnEl = document.getElementById('filterAssignedTo'); var branchEl = document.getElementById('filterBranch'); @@ -4254,7 +4274,6 @@ } else { var parkVal = parkEl.value; if (parkVal && parkVal.startsWith('group:')) { - // Grouped park category assetFilters.disney_group = parkVal.replace('group:', ''); assetFilters.disney_park = null; assetFilters.disney_filter = null; @@ -4269,38 +4288,20 @@ } } assetFilters.customer_id = custEl.value || null; + assetFilters.place = placeEl ? placeEl.value || null : null; + assetFilters.location_text = locTextEl ? locTextEl.value.trim() || null : null; assetFilters.assigned_to = assnEl.value || null; assetFilters.branch = branchEl.value || null; - var prevCust = locEl.dataset.customerId; - if (custEl.value !== prevCust) { - locEl.innerHTML = ''; - locEl.disabled = true; - assetFilters.location_id = null; - if (custEl.value) { - loadLocationsForCustomer(custEl.value); - } - } else { - assetFilters.location_id = locEl.value || null; - } - locEl.dataset.customerId = custEl.value; + // Clear old location_id since we replaced it + assetFilters.location_id = null; assetOffset = 0; renderActiveFilterTags(); _loadAssets(); } - async function loadLocationsForCustomer(customerId) { - try { - var locations = await api('/api/locations?customer_id=' + customerId); - var locEl = document.getElementById('filterLocation'); - locEl.innerHTML = '' + - locations.map(function(l) { return ''; }).join(''); - locEl.disabled = false; - } catch (e) { showToast('Failed to load locations', true); } - } - function getActiveFilterCount() { var count = 0; if (assetFilters.category) count++; @@ -4310,6 +4311,8 @@ if (assetFilters.disney_group) count++; if (assetFilters.disney_filter) count++; if (assetFilters.customer_id) count++; + if (assetFilters.place) count++; + if (assetFilters.location_text) count++; if (assetFilters.location_id) count++; if (assetFilters.assigned_to) count++; if (assetFilters.branch) count++; @@ -4340,8 +4343,10 @@ if (assetFilters.customer_id) { var custSel = document.getElementById('filterCustomer'); var txt = custSel.options[custSel.selectedIndex] ? custSel.options[custSel.selectedIndex].text : 'Customer'; - tags.push({key:'customer_id', label:'\uD83C\uDFE2 ' + txt}); + tags.push({key:'customer_id', label:'🏢 ' + txt}); } + if (assetFilters.place) tags.push({key:'place', label:'📍 ' + assetFilters.place}); + if (assetFilters.location_text) tags.push({key:'location_text', label:'🏗️ ' + assetFilters.location_text}); if (assetFilters.location_id) { var locSel = document.getElementById('filterLocation'); var txt = locSel.options[locSel.selectedIndex] ? locSel.options[locSel.selectedIndex].text : 'Location'; @@ -4385,10 +4390,14 @@ assetFilters.disney_filter = null; } else if (key === 'customer_id') { document.getElementById('filterCustomer').value = ''; - document.getElementById('filterLocation').innerHTML = ''; - document.getElementById('filterLocation').disabled = true; assetFilters.customer_id = null; assetFilters.location_id = null; + } else if (key === 'place') { + document.getElementById('filterPlace').value = ''; + assetFilters.place = null; + } else if (key === 'location_text') { + document.getElementById('filterLocationText').value = ''; + assetFilters.location_text = null; } else if (key === 'location_id') { document.getElementById('filterLocation').value = ''; assetFilters.location_id = null; @@ -4410,7 +4419,7 @@ function clearAllFilters() { 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 }; + customer_id: null, location_id: null, assigned_to: null, branch: null, place: null, location_text: null }; document.getElementById('assetSearch').value = ''; document.getElementById('clearSearch').style.display = 'none'; document.getElementById('filterCategory').value = ''; @@ -4418,8 +4427,8 @@ document.getElementById('filterDisneyPark').value = ''; document.getElementById('filterDisneyFilter').value = ''; document.getElementById('filterCustomer').value = ''; - document.getElementById('filterLocation').innerHTML = ''; - document.getElementById('filterLocation').disabled = true; + if (document.getElementById('filterPlace')) document.getElementById('filterPlace').value = ''; + if (document.getElementById('filterLocationText')) document.getElementById('filterLocationText').value = ''; document.getElementById('filterAssignedTo').value = ''; document.getElementById('filterBranch').value = ''; @@ -4582,11 +4591,13 @@ var techEl = document.getElementById('woFilterTech'); var priorityEl = document.getElementById('woFilterPriority'); var typeEl = document.getElementById('woFilterType'); + var locEl = document.getElementById('woFilterLocation'); woFilters.status = statusEl.value || null; woFilters.tech = techEl.value || null; woFilters.priority = priorityEl.value || null; woFilters.work_type = typeEl.value || null; + woFilters.location = locEl ? locEl.value.trim() || null : null; renderWOActiveFilterTags(); woOffset = 0; @@ -4600,6 +4611,7 @@ if (woFilters.tech) count++; if (woFilters.priority) count++; if (woFilters.work_type) count++; + if (woFilters.location) count++; if (woFilters.date_range && woFilters.date_range !== 'today' && woFilters.date_range !== 'all') count++; if (woFilters.date_from) count++; if (woFilters.date_to) count++; @@ -4624,6 +4636,7 @@ if (woFilters.tech) tags.push({key:'tech', label:'👤 ' + woFilters.tech}); if (woFilters.priority) tags.push({key:'priority', label:'🏷️ ' + woFilters.priority}); if (woFilters.work_type) tags.push({key:'work_type', label:'🔧 ' + woFilters.work_type}); + if (woFilters.location) tags.push({key:'location', label:'📍 ' + woFilters.location}); if (woFilters.date_range === 'today') tags.push({key:'date_range', label:'📅 Today'}); else if (woFilters.date_range === 'week') tags.push({key:'date_range', label:'📅 This Week'}); else if (woFilters.date_range === 'month') tags.push({key:'date_range', label:'📅 This Month'}); @@ -4753,6 +4766,7 @@ // Filters if (woFilters.status) params.set('status', woFilters.status); if (woFilters.tech) params.set('tech', woFilters.tech); + if (woFilters.location) params.set('location', woFilters.location); params.set('limit', String(WO_PAGE_SIZE)); params.set('offset', String(woOffset)); @@ -4804,6 +4818,7 @@ (accountName ? '' : '') + '
' + (dateStr ? '📅 ' + esc(dateStr) + '' : '') + + (w.functional_location ? '📍 ' + esc(w.functional_location) + '' : '') + (w.territory ? '🏢 ' + esc(w.territory) + '' : '') + (techStr ? '' + esc(techStr) + '' : '') + (cityStr ? '📍 ' + esc(cityStr) + '' : '') + @@ -5051,6 +5066,8 @@ 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); + if (assetFilters.place) params.set('place', assetFilters.place); + if (assetFilters.location_text) params.set('location_text', assetFilters.location_text); if (assetFilters.location_id) params.set('location_id', assetFilters.location_id); if (assetFilters.assigned_to) params.set('assigned_to', assetFilters.assigned_to); if (assetFilters.branch) params.set('branch', assetFilters.branch);