Replace broken Location dropdown with Place (city) dropdown + Location Facility text input. Add /api/places endpoint. Add place/location_text filter params to /api/assets. Add functional_location to work orders API and frontend.

This commit is contained in:
2026-07-28 20:34:03 -04:00
parent 7b327b3641
commit 96ed283163
2 changed files with 72 additions and 34 deletions
+21
View File
@@ -930,6 +930,16 @@ def list_makes():
return [r["manufacturer"] for r in rows] 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 ────────────────────────────────────────────── # ─── Task 4: POST /api/assets ──────────────────────────────────────────────
@@ -1067,6 +1077,8 @@ def list_assets(
disney_filter: 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"), disney_group: Optional[str] = Query(None, description="Grouped park categories: park, resort, office, springs, other"),
q: Optional[str] = Query(None), 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), no_dex_days: Optional[int] = Query(None, ge=1),
has_gps: Optional[bool] = Query(None), has_gps: Optional[bool] = Query(None),
branch: Optional[str] = Query(None, description="Filter by branch/territory name"), 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 ?)") 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}%" like = f"%{q}%"
params.extend([like, like, like, like, like, like, like, like]) 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: if no_dex_days is not None:
conditions.append("(dex_report_date IS NULL OR REPLACE(dex_report_date, 'T', ' ') < datetime('now', '-' || ? || ' days'))") conditions.append("(dex_report_date IS NULL OR REPLACE(dex_report_date, 'T', ' ') < datetime('now', '-' || ? || ' days'))")
params.append(no_dex_days) params.append(no_dex_days)
@@ -3659,6 +3678,7 @@ async def workorders_list(
w."msdyn_workordertype!name" AS work_type, w."msdyn_workordertype!name" AS work_type,
w."msdyn_primaryincidenttype!name" AS incident_type, w."msdyn_primaryincidenttype!name" AS incident_type,
w."msdyn_serviceterritory!name" AS territory, w."msdyn_serviceterritory!name" AS territory,
w."msdyn_functionallocation!name" AS functional_location,
w.msdyn_instructions, w.msdyn_instructions,
w.hsl_onsiteduration, w.hsl_onsiteduration,
w.msdyn_address1, w.msdyn_address1,
@@ -3720,6 +3740,7 @@ async def workorders_list(
"incident_type": r["incident_type"], "incident_type": r["incident_type"],
"territory": r["territory"], "territory": r["territory"],
"customer_asset_name": r["customer_asset_name"], "customer_asset_name": r["customer_asset_name"],
"functional_location": r["functional_location"],
"instructions": r["msdyn_instructions"], "instructions": r["msdyn_instructions"],
"onsite_duration": r["hsl_onsiteduration"], "onsite_duration": r["hsl_onsiteduration"],
"datewindow_start": r["msdyn_datewindowstart"], "datewindow_start": r["msdyn_datewindowstart"],
+50 -33
View File
@@ -1367,11 +1367,15 @@
</select> </select>
</div> </div>
<div style="flex:1"> <div style="flex:1">
<div class="fp-label">Location</div> <div class="fp-label">Place / City</div>
<select id="filterLocation" class="input-field" onchange="onFilterChange()"> <select id="filterPlace" class="input-field" onchange="onFilterChange()">
<option value="">All locations</option> <option value="">All places</option>
</select> </select>
</div> </div>
<div style="flex:1">
<div class="fp-label">Location Facility</div>
<input id="filterLocationText" type="text" class="input-field" placeholder="Building, area..." oninput="onFilterChange()" style="font-size:12px;">
</div>
</div> </div>
<div class="fp-row"> <div class="fp-row">
<div style="flex:1"> <div style="flex:1">
@@ -1878,6 +1882,12 @@
<!-- Filter panel (collapsible) --> <!-- Filter panel (collapsible) -->
<div class="filter-panel" id="woFilterPanel"> <div class="filter-panel" id="woFilterPanel">
<div class="fp-row">
<div style="flex:1">
<div class="fp-label">Location / Facility</div>
<input id="woFilterLocation" type="text" class="input-field" placeholder="City, pavilion, area..." oninput="onWOFilterChange()" style="font-size:12px;">
</div>
</div>
<div class="fp-row"> <div class="fp-row">
<div style="flex:1"> <div style="flex:1">
<div class="fp-label">Status</div> <div class="fp-label">Status</div>
@@ -4153,7 +4163,7 @@
// ── Filter state ────────────────────────────────────────────────────── // ── Filter state ──────────────────────────────────────────────────────
let assetFilters = { category: null, status: null, make: null, disney_park: null, disney_group: 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 }; customer_id: null, location_id: null, assigned_to: null, branch: null, place: null, location_text: null };
let noDexFilterActive = false; let noDexFilterActive = false;
let assetOffset = 0; let assetOffset = 0;
const ASSET_PAGE_SIZE = 2000; const ASSET_PAGE_SIZE = 2000;
@@ -4196,6 +4206,15 @@
var custSel = document.getElementById('filterCustomer'); var custSel = document.getElementById('filterCustomer');
custSel.innerHTML = '<option value="">All customers</option>' + custSel.innerHTML = '<option value="">All customers</option>' +
customers.map(function(c) { return '<option value="' + c.id + '">' + esc(c.name) + '</option>'; }).join(''); customers.map(function(c) { return '<option value="' + c.id + '">' + esc(c.name) + '</option>'; }).join('');
// Load places
try {
var places = await api('/api/places');
var placeSel = document.getElementById('filterPlace');
if (placeSel) placeSel.innerHTML = '<option value="">All places</option>' +
places.map(function(p) { return '<option value="' + esc(p) + '">' + esc(p) + '</option>'; }).join('');
} catch(e) {}
// Load users for assigned_to filter // Load users for assigned_to filter
var users = await api('/api/users'); var users = await api('/api/users');
var assnSel = document.getElementById('filterAssignedTo'); var assnSel = document.getElementById('filterAssignedTo');
@@ -4238,7 +4257,8 @@
var disneyFilterEl = document.getElementById('filterDisneyFilter'); var disneyFilterEl = document.getElementById('filterDisneyFilter');
var parkEl = document.getElementById('filterDisneyPark'); var parkEl = document.getElementById('filterDisneyPark');
var custEl = document.getElementById('filterCustomer'); 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 assnEl = document.getElementById('filterAssignedTo');
var branchEl = document.getElementById('filterBranch'); var branchEl = document.getElementById('filterBranch');
@@ -4254,7 +4274,6 @@
} else { } else {
var parkVal = parkEl.value; var parkVal = parkEl.value;
if (parkVal && parkVal.startsWith('group:')) { if (parkVal && parkVal.startsWith('group:')) {
// Grouped park category
assetFilters.disney_group = parkVal.replace('group:', ''); assetFilters.disney_group = parkVal.replace('group:', '');
assetFilters.disney_park = null; assetFilters.disney_park = null;
assetFilters.disney_filter = null; assetFilters.disney_filter = null;
@@ -4269,38 +4288,20 @@
} }
} }
assetFilters.customer_id = custEl.value || null; 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.assigned_to = assnEl.value || null;
assetFilters.branch = branchEl.value || null; assetFilters.branch = branchEl.value || null;
var prevCust = locEl.dataset.customerId; // Clear old location_id since we replaced it
if (custEl.value !== prevCust) {
locEl.innerHTML = '<option value="">All locations</option>';
locEl.disabled = true;
assetFilters.location_id = null; assetFilters.location_id = null;
if (custEl.value) {
loadLocationsForCustomer(custEl.value);
}
} else {
assetFilters.location_id = locEl.value || null;
}
locEl.dataset.customerId = custEl.value;
assetOffset = 0; assetOffset = 0;
renderActiveFilterTags(); renderActiveFilterTags();
_loadAssets(); _loadAssets();
} }
async function loadLocationsForCustomer(customerId) {
try {
var locations = await api('/api/locations?customer_id=' + customerId);
var locEl = document.getElementById('filterLocation');
locEl.innerHTML = '<option value="">All locations</option>' +
locations.map(function(l) { return '<option value="' + l.id + '">' + esc(l.name) + '</option>'; }).join('');
locEl.disabled = false;
} catch (e) { showToast('Failed to load locations', true); }
}
function getActiveFilterCount() { function getActiveFilterCount() {
var count = 0; var count = 0;
if (assetFilters.category) count++; if (assetFilters.category) count++;
@@ -4310,6 +4311,8 @@
if (assetFilters.disney_group) count++; if (assetFilters.disney_group) count++;
if (assetFilters.disney_filter) count++; if (assetFilters.disney_filter) count++;
if (assetFilters.customer_id) count++; if (assetFilters.customer_id) count++;
if (assetFilters.place) count++;
if (assetFilters.location_text) count++;
if (assetFilters.location_id) count++; if (assetFilters.location_id) count++;
if (assetFilters.assigned_to) count++; if (assetFilters.assigned_to) count++;
if (assetFilters.branch) count++; if (assetFilters.branch) count++;
@@ -4340,8 +4343,10 @@
if (assetFilters.customer_id) { if (assetFilters.customer_id) {
var custSel = document.getElementById('filterCustomer'); var custSel = document.getElementById('filterCustomer');
var txt = custSel.options[custSel.selectedIndex] ? custSel.options[custSel.selectedIndex].text : 'Customer'; 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) { if (assetFilters.location_id) {
var locSel = document.getElementById('filterLocation'); var locSel = document.getElementById('filterLocation');
var txt = locSel.options[locSel.selectedIndex] ? locSel.options[locSel.selectedIndex].text : 'Location'; var txt = locSel.options[locSel.selectedIndex] ? locSel.options[locSel.selectedIndex].text : 'Location';
@@ -4385,10 +4390,14 @@
assetFilters.disney_filter = null; assetFilters.disney_filter = null;
} else if (key === 'customer_id') { } else if (key === 'customer_id') {
document.getElementById('filterCustomer').value = ''; document.getElementById('filterCustomer').value = '';
document.getElementById('filterLocation').innerHTML = '<option value="">All locations</option>';
document.getElementById('filterLocation').disabled = true;
assetFilters.customer_id = null; assetFilters.customer_id = null;
assetFilters.location_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') { } else if (key === 'location_id') {
document.getElementById('filterLocation').value = ''; document.getElementById('filterLocation').value = '';
assetFilters.location_id = null; assetFilters.location_id = null;
@@ -4410,7 +4419,7 @@
function clearAllFilters() { function clearAllFilters() {
assetFilters = { category: null, status: null, make: null, disney_park: null, disney_group: 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 }; customer_id: null, location_id: null, assigned_to: null, branch: null, place: null, location_text: null };
document.getElementById('assetSearch').value = ''; document.getElementById('assetSearch').value = '';
document.getElementById('clearSearch').style.display = 'none'; document.getElementById('clearSearch').style.display = 'none';
document.getElementById('filterCategory').value = ''; document.getElementById('filterCategory').value = '';
@@ -4418,8 +4427,8 @@
document.getElementById('filterDisneyPark').value = ''; document.getElementById('filterDisneyPark').value = '';
document.getElementById('filterDisneyFilter').value = ''; document.getElementById('filterDisneyFilter').value = '';
document.getElementById('filterCustomer').value = ''; document.getElementById('filterCustomer').value = '';
document.getElementById('filterLocation').innerHTML = '<option value="">All locations</option>'; if (document.getElementById('filterPlace')) document.getElementById('filterPlace').value = '';
document.getElementById('filterLocation').disabled = true; if (document.getElementById('filterLocationText')) document.getElementById('filterLocationText').value = '';
document.getElementById('filterAssignedTo').value = ''; document.getElementById('filterAssignedTo').value = '';
document.getElementById('filterBranch').value = ''; document.getElementById('filterBranch').value = '';
@@ -4582,11 +4591,13 @@
var techEl = document.getElementById('woFilterTech'); var techEl = document.getElementById('woFilterTech');
var priorityEl = document.getElementById('woFilterPriority'); var priorityEl = document.getElementById('woFilterPriority');
var typeEl = document.getElementById('woFilterType'); var typeEl = document.getElementById('woFilterType');
var locEl = document.getElementById('woFilterLocation');
woFilters.status = statusEl.value || null; woFilters.status = statusEl.value || null;
woFilters.tech = techEl.value || null; woFilters.tech = techEl.value || null;
woFilters.priority = priorityEl.value || null; woFilters.priority = priorityEl.value || null;
woFilters.work_type = typeEl.value || null; woFilters.work_type = typeEl.value || null;
woFilters.location = locEl ? locEl.value.trim() || null : null;
renderWOActiveFilterTags(); renderWOActiveFilterTags();
woOffset = 0; woOffset = 0;
@@ -4600,6 +4611,7 @@
if (woFilters.tech) count++; if (woFilters.tech) count++;
if (woFilters.priority) count++; if (woFilters.priority) count++;
if (woFilters.work_type) 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_range && woFilters.date_range !== 'today' && woFilters.date_range !== 'all') count++;
if (woFilters.date_from) count++; if (woFilters.date_from) count++;
if (woFilters.date_to) count++; if (woFilters.date_to) count++;
@@ -4624,6 +4636,7 @@
if (woFilters.tech) tags.push({key:'tech', label:'👤 ' + woFilters.tech}); if (woFilters.tech) tags.push({key:'tech', label:'👤 ' + woFilters.tech});
if (woFilters.priority) tags.push({key:'priority', label:'🏷️ ' + woFilters.priority}); 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.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'}); 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 === 'week') tags.push({key:'date_range', label:'📅 This Week'});
else if (woFilters.date_range === 'month') tags.push({key:'date_range', label:'📅 This Month'}); else if (woFilters.date_range === 'month') tags.push({key:'date_range', label:'📅 This Month'});
@@ -4753,6 +4766,7 @@
// Filters // Filters
if (woFilters.status) params.set('status', woFilters.status); if (woFilters.status) params.set('status', woFilters.status);
if (woFilters.tech) params.set('tech', woFilters.tech); 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('limit', String(WO_PAGE_SIZE));
params.set('offset', String(woOffset)); params.set('offset', String(woOffset));
@@ -4804,6 +4818,7 @@
(accountName ? '<div class="wo-account">' + esc(accountName) + '</div>' : '') + (accountName ? '<div class="wo-account">' + esc(accountName) + '</div>' : '') +
'<div class="wo-meta">' + '<div class="wo-meta">' +
(dateStr ? '<span>📅 ' + esc(dateStr) + '</span>' : '') + (dateStr ? '<span>📅 ' + esc(dateStr) + '</span>' : '') +
(w.functional_location ? '<span>📍 ' + esc(w.functional_location) + '</span>' : '') +
(w.territory ? '<span>🏢 ' + esc(w.territory) + '</span>' : '') + (w.territory ? '<span>🏢 ' + esc(w.territory) + '</span>' : '') +
(techStr ? '<span>' + esc(techStr) + '</span>' : '') + (techStr ? '<span>' + esc(techStr) + '</span>' : '') +
(cityStr ? '<span>📍 ' + esc(cityStr) + '</span>' : '') + (cityStr ? '<span>📍 ' + esc(cityStr) + '</span>' : '') +
@@ -5051,6 +5066,8 @@
if (assetFilters.disney_filter) params.set('disney_filter', assetFilters.disney_filter); if (assetFilters.disney_filter) params.set('disney_filter', assetFilters.disney_filter);
if (assetFilters.q) params.set('q', assetFilters.q); if (assetFilters.q) params.set('q', assetFilters.q);
if (assetFilters.customer_id) params.set('customer_id', assetFilters.customer_id); 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.location_id) params.set('location_id', assetFilters.location_id);
if (assetFilters.assigned_to) params.set('assigned_to', assetFilters.assigned_to); if (assetFilters.assigned_to) params.set('assigned_to', assetFilters.assigned_to);
if (assetFilters.branch) params.set('branch', assetFilters.branch); if (assetFilters.branch) params.set('branch', assetFilters.branch);