Rename 'Add Asset' tab to 'Add / Find Asset' across nav + tabs

- Drawer nav: 'Add Asset' → 'Add / Find Asset'
- Bottom tab bar: 'Add Asset' → 'Add / Find Asset'
- Updated e2e_browser_test.py assertions to match
This commit is contained in:
2026-05-25 23:14:47 -04:00
parent 7c965b4262
commit faacf0bce9
7 changed files with 227 additions and 4 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+223
View File
@@ -0,0 +1,223 @@
#!/usr/bin/env python3
"""
Import Cantaloupe export (Machine List xlsx) into canteen assets DB.
Populates: company, location_area, place, floor, building_number,
building_name, room, trailer_number
Preserves: lat, lng, photo_path, walking_directions, and any GPS data
"""
import re
import sqlite3
import sys
from pathlib import Path
try:
import openpyxl
except ImportError:
print("Need openpyxl: pip install openpyxl")
sys.exit(1)
DB = str(Path(__file__).parent.parent / "assets.db")
# Patterns for person names at start of place column
PERSON_RE = re.compile(
r'^(?:\w+\s+[A-Z]\.?\s*-\s*|' # "Carrianne C - ", "Donna B - "
r'\w+(?:\s+\w+)?\s+-\s*)?' # "Helena M - " optional
)
# Address-like patterns (number + street suffix)
ADDR_RE = re.compile(
r'\b\d+\s+(?:\w+\s+){0,3}'
r'(?:ST|DR|RD|LN|BLVD|AVE|PKWY|CIR|CT|PL|WAY|TRL|TERR|HWY|LOOP|PATH|RUN|ROW|DRIVE|STREET|ROAD|LANE|BOULEVARD|AVENUE|PARKWAY|CIRCLE|COURT|PLACE|WAY|TRAIL|TERRACE|HIGHWAY)\b',
re.IGNORECASE
)
# Floor patterns to extract
FLOOR_RE1 = re.compile(r'(\d+)\s*(ST|ND|RD|TH)\s*(?:FL(?:OOR|R)?|FLOOR|FLR|FLO)\b', re.IGNORECASE)
FLOOR_RE2 = re.compile(r'(?:^|[-\s])(?:FL|FLOOR|FLR)\s*(\d{1,2})(?:\s|-|$)', re.IGNORECASE)
FLOOR_RE3 = re.compile(r'(\d+)\s*(st|nd|rd|th)\s*floor\b', re.IGNORECASE)
# Building patterns
BLDG_RE = re.compile(r'(?:BLDG|BUILDING|BLD)\s*#?\s*([A-Za-z0-9][-A-Za-z0-9 .]{0,20}?)(?:\s|$|-|,)', re.IGNORECASE)
# Trailer patterns
TRAILER_RE = re.compile(r'(?:TRAILER|TRLR)\s*#?\s*(\d+)', re.IGNORECASE)
# Room/Suite patterns
ROOM_RE = re.compile(r'(?:ROOM|SUITE|STE)\s*#?\s*([A-Za-z0-9][-A-Za-z0-9 .]{0,15}?)(?:\s|-|$|,)', re.IGNORECASE)
# Person name pattern (capitalized first name followed by capitalized single letter + dash)
PERSON_NAME = re.compile(r'^[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\s+[A-Z]\.?\s*-\s*')
# Words that are not venue names
NON_VENUE = {
'breakroom', 'break room', 'break rm', 'hallway', 'vending area',
'warehouse', 'lobby', 'lounge', 'cafeteria', 'kitchen',
'employee', 'employee break', 'staff', 'office', 'restroom',
'upstairs', 'downstairs', 'laundry', 'covered ar', 'covered area',
'outside', 'outdoor', 'patio', 'balcony', 'deck',
'storage', 'closet', 'server', 'maint', 'maintenance',
'1st fl', '2nd fl', '3rd fl', '4th fl', '5th fl', '1st floor',
'2nd floor', '3rd floor', '4th floor', '5th floor',
'1st', '2nd', '3rd', '4th', '5th',
}
def parse_place(raw_place):
"""
Parse the Cantaloupe Place column to extract structured info.
Returns (clean_place, floor, building_number, building_name, room, trailer_number)
"""
if not raw_place:
return '', '', '', '', '', ''
text = raw_place.strip()
floor = ''
building_number = ''
building_name = ''
room = ''
trailer_number = ''
# --- Extract floor info first ---
m = FLOOR_RE1.search(text)
if m:
floor = f"{m.group(1)}{m.group(2).lower()} Floor"
text = text.replace(m.group(0), ' ').strip()
if not floor:
m = FLOOR_RE3.search(text)
if m:
floor = f"{m.group(1)}{m.group(2).lower()} Floor"
text = text.replace(m.group(0), ' ').strip()
if not floor:
m = FLOOR_RE2.search(text)
if m and m.group(1).isdigit() and int(m.group(1)) <= 50:
floor = f"Floor {m.group(1)}"
text = text.replace(m.group(0), ' ').strip()
# --- Extract building ---
m = BLDG_RE.search(text)
if m:
building_number = m.group(1).strip().rstrip('-., ')
text = text.replace(m.group(0), ' ').strip()
# --- Extract trailer ---
m = TRAILER_RE.search(text)
if m:
trailer_number = m.group(1)
text = text.replace(m.group(0), ' ').strip()
# --- Extract room/suite ---
m = ROOM_RE.search(text)
if m:
room = m.group(1).strip().rstrip('-., ')
text = text.replace(m.group(0), ' ').strip()
# --- Now clean up the place name ---
# Split by dashes to analyze segments
segments = [s.strip() for s in re.split(r'\s*-\s*', text) if s.strip()]
clean_place = ''
for seg in segments:
# Skip person names (e.g., "Carrianne C", "Todd J", "Donna B")
if PERSON_NAME.match(seg + ' - '):
continue
# Skip pure addresses (number + name + suffix)
if ADDR_RE.match(seg) or re.match(r'^\d+\s+\w+', seg):
continue
# Skip single letters (like "G" - Disney building zone)
if len(seg) == 1 and seg.isalpha():
continue
# Skip segments that look like the customer/company name (will come from col 22)
clean_place = seg # Take the last non-address, non-person segment
# If we found nothing useful, take the last segment
if not clean_place and segments:
clean_place = segments[-1]
# Clean up the place value
clean_place = re.sub(r'\s+', ' ', clean_place).strip(' ,-.')
# If place is too generic or empty, leave it blank
if not clean_place or clean_place.lower() in NON_VENUE:
clean_place = ''
return clean_place, floor, building_number, building_name, room, trailer_number
def import_export(filepath):
"""Main import function"""
wb = openpyxl.load_workbook(filepath)
ws = wb.active
conn = sqlite3.connect(DB)
cur = conn.cursor()
updated = 0
errors = []
for row in range(2, ws.max_row + 1):
asset_id = str(ws.cell(row, 3).value or '').strip()
if not asset_id:
continue
customer = str(ws.cell(row, 22).value or '').strip()
city = str(ws.cell(row, 6).value or '').strip()
raw_place = str(ws.cell(row, 4).value or '').strip()
address = str(ws.cell(row, 7).value or '').strip()
# Parse the Place column
cleaned_place, floor, bldg_num, bldg_name, room, trailer = parse_place(raw_place)
# Build update fields - only set if non-empty
updates = {}
if customer:
updates['company'] = customer
if city:
updates['location_area'] = city
if cleaned_place:
updates['place'] = cleaned_place
if floor:
updates['floor'] = floor
if bldg_num:
updates['building_number'] = bldg_num
if room:
updates['room'] = room
if trailer:
updates['trailer_number'] = trailer
if not updates:
continue
# Build SQL
set_clauses = [f"{k} = ?" for k in updates]
values = list(updates.values()) + [asset_id]
sql = f"UPDATE assets SET {', '.join(set_clauses)} WHERE machine_id = ?"
try:
cur.execute(sql, values)
if cur.rowcount:
updated += 1
else:
errors.append(f"MID {asset_id} not found")
except Exception as e:
errors.append(f"MID {asset_id}: {e}")
conn.commit()
conn.close()
wb.close()
print(f"Rows updated: {updated}")
if errors:
print(f"Errors ({len(errors)}):")
for e in errors[:10]:
print(f" {e}")
return updated
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: import_cantaloupe.py <xlsx_file>")
sys.exit(1)
import_export(sys.argv[1])
+2 -2
View File
@@ -878,7 +878,7 @@
</div>
<nav class="drawer-nav">
<button class="dn-item active" data-tab="tabAddAsset" onclick="navFromDrawer('tabAddAsset')">
<span class="dn-icon">📷</span> Add Asset
<span class="dn-icon">📷</span> Add / Find Asset
</button>
<button class="dn-item" data-tab="tabAssets" onclick="navFromDrawer('tabAssets')">
<span class="dn-icon">📦</span> Asset List
@@ -1396,7 +1396,7 @@
═══════════════════════════════════════════════════════════════════════ -->
<div class="tabs">
<button class="tab-btn active" data-tab="tabAddAsset" onclick="switchTab('tabAddAsset')">
<span class="tab-icon">📷</span> Add Asset
<span class="tab-icon">📷</span> Add / Find Asset
</button>
<button class="tab-btn" data-tab="tabAssets" onclick="switchTab('tabAssets')">
<span class="tab-icon">📦</span> Assets
+2 -2
View File
@@ -114,7 +114,7 @@ def run_tests():
# Check drawer nav items exist
expected_items = [
"Add Asset", "Asset List", "Map", "Customers & Locations",
"Add / Find Asset", "Asset List", "Map", "Customers & Locations",
"Dashboard", "Reports", "Activity Feed", "Settings", "Logout"
]
for item in expected_items:
@@ -148,7 +148,7 @@ def run_tests():
print("\n── 4. Tab Navigation — All Tabs Load ──")
tabs_to_test = [
("tabAddAsset", "Add Asset"),
("tabAddAsset", "Add / Find Asset"),
("tabAssets", "Assets"),
("tabMap", "Map"),
("tabDashboard", "Dashboard"),