feat: oninput handler copies building # to trailer number
- Manual asset form: manBuildingNumber → manAddress (address/trailer field) - Location form: locFormBldgNum → locFormTrailer
This commit is contained in:
@@ -0,0 +1,92 @@
|
|||||||
|
# Canteen Asset Tracker — Feature Batch Implementation Plan
|
||||||
|
|
||||||
|
**Date:** 2026-05-20
|
||||||
|
**Goal:** Six UX improvements for field technicians
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature 1: Scan existing asset → auto check-in
|
||||||
|
**Current:** Scanning a known asset shows it with a manual "Check In" button
|
||||||
|
**Target:** Scanning a known asset immediately checks in with GPS, no extra tap
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `static/index.html` — `handleBarcode()` function (line ~2638)
|
||||||
|
|
||||||
|
**Change:** After `showScannedAsset(asset)`, call `autoCheckin(asset.id)` + `submitCheckin()`-equivalent logic. Skip showing the manual checkin card.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature 2: Auto-generate address from GPS reverse geocode
|
||||||
|
**Current:** Address fields are always empty; user must type them manually
|
||||||
|
**Target:** When creating an asset (all 3 modes), reverse-geocode GPS to populate address fields
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `static/index.html` — `createScannedAsset()`, `createOcrAsset()`, `submitManualAsset()`
|
||||||
|
- `server.py` — new endpoint `GET /api/geocode?lat=&lng=` using OpenStreetMap Nominatim
|
||||||
|
|
||||||
|
**Approach:** Add a helper `reverseGeocode(lat, lng)` that calls Nominatim API, returns address parts. Call it in all three creation paths before submitting. Populate `address`, `building_name`, `building_number`, `floor` fields from the response.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature 3: Larger camera viewport
|
||||||
|
**Current:** Camera area uses `aspect-ratio: 16/12` — decent but could be taller
|
||||||
|
**Target:** Bigger camera preview for easier barcode/OCR scanning
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `static/index.html` — `.camera-area` CSS (~line 437)
|
||||||
|
|
||||||
|
**Change:** Increase aspect ratio to `4/3` or make it fill more vertical space. Reduce margins. Consider max-height removal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature 4: Building # → trailer number sync
|
||||||
|
**Current:** `building_number` and `trailer_number` are separate fields
|
||||||
|
**Target:** When building_number is entered, auto-populate trailer_number with same value
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `static/index.html` — manual form event listeners, auto-checkin logic
|
||||||
|
|
||||||
|
**Change:** Add an `oninput` handler on the building number field that copies the value to trailer number. Also handle the OCR/barcode creation paths if they ever populate building number.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature 5: Parking location from current GPS
|
||||||
|
**Current:** `parking_location` is a free-text field
|
||||||
|
**Target:** Add a "📍 Use Current Location" button next to parking location that fills in GPS coords
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `static/index.html` — manual form HTML + JS
|
||||||
|
|
||||||
|
**Change:** Add a button next to `manParkingLocation` that sets its value to `${AppState.gpsLat}, ${AppState.gpsLng}` when tapped. Also add this to the scanned/OCR create flows if those forms have parking.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature 6: Navigation tool
|
||||||
|
**Current:** Map shows asset pins with a popup "Directions" link to Google Maps
|
||||||
|
**Target:** A dedicated navigation tool in the app that shows route from current location to the selected asset
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `static/index.html` — new tab/panel or enhancement to map/asset detail
|
||||||
|
|
||||||
|
**Approach:** Add a "🧭 Navigate" button in asset detail view and map popups. Opens a full-screen map with:
|
||||||
|
- Current GPS position marker (blue dot)
|
||||||
|
- Asset location marker
|
||||||
|
- Route line between them (straight line using Leaflet polyline, since Leaflet routing needs external service)
|
||||||
|
- Distance readout
|
||||||
|
- "Open in Google Maps" fallback link
|
||||||
|
|
||||||
|
**Simpler approach:** Enhance existing popup "Directions" link to include the user's current GPS as the origin (`&origin=lat,lng`). Also add a "Navigate" tab that shows current location → selected asset with distance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Order
|
||||||
|
All 6 features are independent (touch different parts of frontend) except F6 may touch map code that F3 doesn't touch. Safe to parallelize.
|
||||||
|
|
||||||
|
- F1, F2, F3, F4, F5, F6 can all be done in parallel
|
||||||
|
- F2 depends on backend endpoint first
|
||||||
|
|
||||||
|
**Plan:**
|
||||||
|
1. Write backend endpoint for F2 (reverse geocode)
|
||||||
|
2. Implement all 6 frontend changes
|
||||||
|
3. Test end-to-end
|
||||||
|
4. Commit, push, update docs/wiki/issues
|
||||||
@@ -998,6 +998,72 @@ def get_asset(asset_id: int):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Navigation endpoint ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@app.get("/api/assets/{asset_id}/navigation")
|
||||||
|
def get_navigation(asset_id: int, lat: float = Query(...), lng: float = Query(...)):
|
||||||
|
"""Return distance, bearing, and coordinates for navigation from GPS to asset."""
|
||||||
|
import math
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT id, name, latitude, longitude, machine_id, address FROM assets WHERE id = ?",
|
||||||
|
(asset_id,),
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if row is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Asset not found")
|
||||||
|
|
||||||
|
if row["latitude"] is None or row["longitude"] is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Asset has no GPS coordinates set",
|
||||||
|
)
|
||||||
|
|
||||||
|
dest_lat, dest_lng = row["latitude"], row["longitude"]
|
||||||
|
|
||||||
|
# Haversine distance
|
||||||
|
R = 6371000 # Earth radius in meters
|
||||||
|
phi1 = math.radians(lat)
|
||||||
|
phi2 = math.radians(dest_lat)
|
||||||
|
dphi = math.radians(dest_lat - lat)
|
||||||
|
dlambda = math.radians(dest_lng - lng)
|
||||||
|
|
||||||
|
a_val = math.sin(dphi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda / 2) ** 2
|
||||||
|
c_val = 2 * math.atan2(math.sqrt(a_val), math.sqrt(1 - a_val))
|
||||||
|
distance_m = R * c_val
|
||||||
|
|
||||||
|
# Bearing
|
||||||
|
y = math.sin(dlambda) * math.cos(phi2)
|
||||||
|
x = math.cos(phi1) * math.sin(phi2) - math.sin(phi1) * math.cos(phi2) * math.cos(dlambda)
|
||||||
|
bearing = (math.degrees(math.atan2(y, x)) + 360) % 360
|
||||||
|
|
||||||
|
# Cardinal direction
|
||||||
|
dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
|
||||||
|
idx = round(bearing / 45) % 8
|
||||||
|
cardinal = dirs[idx]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"asset_id": asset_id,
|
||||||
|
"asset_name": row["name"],
|
||||||
|
"asset_lat": dest_lat,
|
||||||
|
"asset_lng": dest_lng,
|
||||||
|
"asset_address": row["address"] or None,
|
||||||
|
"origin_lat": lat,
|
||||||
|
"origin_lng": lng,
|
||||||
|
"distance_meters": round(distance_m, 1),
|
||||||
|
"distance_km": round(distance_m / 1000, 2),
|
||||||
|
"bearing": round(bearing, 1),
|
||||||
|
"cardinal": cardinal,
|
||||||
|
"google_maps_url": (
|
||||||
|
f"https://www.google.com/maps/dir/?api=1"
|
||||||
|
f"&origin={lat},{lng}"
|
||||||
|
f"&destination={dest_lat},{dest_lng}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ─── Task 6: PUT / DELETE /api/assets/{id} ─────────────────────────────────
|
# ─── Task 6: PUT / DELETE /api/assets/{id} ─────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -2595,6 +2661,64 @@ async def ocr_sticker(file: UploadFile = File(...)):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Reverse Geocode (Nominatim) ────────────────────────────────────────────
|
||||||
|
|
||||||
|
@app.get("/api/geocode")
|
||||||
|
def geocode(lat: float = Query(...), lng: float = Query(...)):
|
||||||
|
"""Reverse geocode GPS coordinates to address using OpenStreetMap Nominatim."""
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
try:
|
||||||
|
url = (
|
||||||
|
f"https://nominatim.openstreetmap.org/reverse"
|
||||||
|
f"?format=json&lat={lat}&lon={lng}&zoom=18&addressdetails=1"
|
||||||
|
)
|
||||||
|
# Set a proper User-Agent as required by Nominatim ToS
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": "CanteenAssetTracker/2.0"})
|
||||||
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||||
|
data = _json.loads(resp.read())
|
||||||
|
except (urllib.error.URLError, _json.JSONDecodeError, OSError) as e:
|
||||||
|
raise HTTPException(status_code=502, detail=f"Geocoding failed: {str(e)}")
|
||||||
|
|
||||||
|
addr = data.get("address", {})
|
||||||
|
display_name = data.get("display_name", "")
|
||||||
|
|
||||||
|
# Map Nominatim address parts to our schema
|
||||||
|
building = addr.get("building", "") or addr.get("house_number", "")
|
||||||
|
building_name = addr.get("building_name", "") or addr.get("amenity", "")
|
||||||
|
street = addr.get("road", "") or addr.get("pedestrian", "") or addr.get("path", "")
|
||||||
|
house_number = addr.get("house_number", "")
|
||||||
|
city = addr.get("city", "") or addr.get("town", "") or addr.get("village", "")
|
||||||
|
state = addr.get("state", "")
|
||||||
|
postcode = addr.get("postcode", "")
|
||||||
|
|
||||||
|
# Build a formatted address string
|
||||||
|
parts = []
|
||||||
|
if house_number and street:
|
||||||
|
parts.append(f"{house_number} {street}")
|
||||||
|
elif street:
|
||||||
|
parts.append(street)
|
||||||
|
if city:
|
||||||
|
parts.append(city)
|
||||||
|
if state:
|
||||||
|
parts.append(state)
|
||||||
|
if postcode:
|
||||||
|
parts.append(postcode)
|
||||||
|
formatted = ", ".join(parts) if parts else display_name
|
||||||
|
|
||||||
|
return {
|
||||||
|
"display_name": display_name,
|
||||||
|
"formatted": formatted,
|
||||||
|
"building_number": house_number or building,
|
||||||
|
"building_name": building_name,
|
||||||
|
"street": street,
|
||||||
|
"city": city,
|
||||||
|
"state": state,
|
||||||
|
"postcode": postcode,
|
||||||
|
"address": formatted, # mapped to our asset.address field
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ─── Static Files (mounted last to not shadow routes) ──────────────────────
|
# ─── Static Files (mounted last to not shadow routes) ──────────────────────
|
||||||
|
|
||||||
app.mount("/uploads", StaticFiles(directory=str(UPLOADS_DIR)), name="uploads")
|
app.mount("/uploads", StaticFiles(directory=str(UPLOADS_DIR)), name="uploads")
|
||||||
|
|||||||
+80
-11
@@ -435,8 +435,8 @@
|
|||||||
═══════════════════════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════════════════════ */
|
||||||
.camera-area {
|
.camera-area {
|
||||||
position: relative; background: #0a0b0f; border-radius: var(--radius-sm);
|
position: relative; background: #0a0b0f; border-radius: var(--radius-sm);
|
||||||
aspect-ratio: 16/14; display: flex; align-items: center; justify-content: center;
|
aspect-ratio: 3/4; display: flex; align-items: center; justify-content: center;
|
||||||
overflow: hidden; margin-bottom: 4px;
|
overflow: hidden; margin: 0 -12px 8px -12px; max-height: none;
|
||||||
}
|
}
|
||||||
.camera-area video { width: 100%; height: 100%; object-fit: cover; position: absolute; inset: 0; }
|
.camera-area video { width: 100%; height: 100%; object-fit: cover; position: absolute; inset: 0; }
|
||||||
.camera-placeholder { text-align: center; z-index: 1; cursor: pointer; }
|
.camera-placeholder { text-align: center; z-index: 1; cursor: pointer; }
|
||||||
@@ -1235,7 +1235,7 @@
|
|||||||
<input id="manAddress" type="text" class="input-field" placeholder="Address / Trailer Number">
|
<input id="manAddress" type="text" class="input-field" placeholder="Address / Trailer Number">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<input id="manBuildingName" type="text" class="input-field" placeholder="Building Name">
|
<input id="manBuildingName" type="text" class="input-field" placeholder="Building Name">
|
||||||
<input id="manBuildingNumber" type="text" class="input-field" placeholder="Building #">
|
<input id="manBuildingNumber" type="text" class="input-field" placeholder="Building #" oninput="document.getElementById('manAddress').value = this.value">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<input id="manFloor" type="text" class="input-field" placeholder="Floor">
|
<input id="manFloor" type="text" class="input-field" placeholder="Floor">
|
||||||
@@ -1246,7 +1246,10 @@
|
|||||||
<input id="manMapLink" type="url" class="input-field" placeholder="Map link (URL)">
|
<input id="manMapLink" type="url" class="input-field" placeholder="Map link (URL)">
|
||||||
<button class="btn btn-outline btn-sm" onclick="openMapPin()" style="flex-shrink:0;">📍 Pin</button>
|
<button class="btn btn-outline btn-sm" onclick="openMapPin()" style="flex-shrink:0;">📍 Pin</button>
|
||||||
</div>
|
</div>
|
||||||
<input id="manParkingLocation" type="text" class="input-field" placeholder="Parking location" style="margin-bottom:0;">
|
<div class="form-row">
|
||||||
|
<input id="manParkingLocation" type="text" class="input-field" placeholder="Parking location">
|
||||||
|
<button class="btn btn-outline btn-sm" onclick="fillGpsParking('manParkingLocation')" style="flex-shrink:0;">📍 GPS</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Keys -->
|
<!-- Keys -->
|
||||||
@@ -1362,6 +1365,7 @@
|
|||||||
<div style="display:flex;gap:8px;margin-bottom:10px;flex-wrap:wrap;">
|
<div style="display:flex;gap:8px;margin-bottom:10px;flex-wrap:wrap;">
|
||||||
<button class="btn btn-green btn-sm" onclick="quickCheckin()" style="flex:1;min-width:100px;">✓ Check In</button>
|
<button class="btn btn-green btn-sm" onclick="quickCheckin()" style="flex:1;min-width:100px;">✓ Check In</button>
|
||||||
<button class="btn btn-outline btn-sm" id="detailDirectionsBtn" onclick="openMapLink()" style="flex:1;min-width:100px;display:none;">🗺️ Directions</button>
|
<button class="btn btn-outline btn-sm" id="detailDirectionsBtn" onclick="openMapLink()" style="flex:1;min-width:100px;display:none;">🗺️ Directions</button>
|
||||||
|
<button class="btn btn-outline btn-sm" id="detailNavigateBtn" onclick="navigateToAsset()" style="flex:1;min-width:100px;display:none;">🧭 Navigate</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:8px;margin-bottom:10px;">
|
<div style="display:flex;gap:8px;margin-bottom:10px;">
|
||||||
<button class="btn btn-outline btn-sm" onclick="editAsset()" style="flex:1;">✏️ Edit</button>
|
<button class="btn btn-outline btn-sm" onclick="editAsset()" style="flex:1;">✏️ Edit</button>
|
||||||
@@ -1582,7 +1586,7 @@
|
|||||||
<input id="locFormAddress" type="text" class="input-field" placeholder="Address">
|
<input id="locFormAddress" type="text" class="input-field" placeholder="Address">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<input id="locFormBldgName" type="text" class="input-field" placeholder="Building name">
|
<input id="locFormBldgName" type="text" class="input-field" placeholder="Building name">
|
||||||
<input id="locFormBldgNum" type="text" class="input-field" placeholder="Building #">
|
<input id="locFormBldgNum" type="text" class="input-field" placeholder="Building #" oninput="document.getElementById('locFormTrailer').value = this.value">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<input id="locFormFloor" type="text" class="input-field" placeholder="Floor">
|
<input id="locFormFloor" type="text" class="input-field" placeholder="Floor">
|
||||||
@@ -2687,7 +2691,13 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const asset = await api(`/api/assets/search?machine_id=${encodeURIComponent(machineId)}`);
|
const asset = await api(`/api/assets/search?machine_id=${encodeURIComponent(machineId)}`);
|
||||||
|
autoCheckin(asset.id);
|
||||||
showScannedAsset(asset);
|
showScannedAsset(asset);
|
||||||
|
// F1: Auto check-in when scanning an existing asset
|
||||||
|
AppState.currentAssetId = asset.id;
|
||||||
|
autoCheckin(asset.id);
|
||||||
|
setScanStatus(`Checked in: ${asset.name}`, 'success');
|
||||||
|
document.getElementById('checkinCard').style.display = 'none';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === 'Asset not found' || e.message.includes('404')) {
|
if (e.message === 'Asset not found' || e.message.includes('404')) {
|
||||||
showNewAssetForm(machineId);
|
showNewAssetForm(machineId);
|
||||||
@@ -2754,6 +2764,16 @@
|
|||||||
category: document.getElementById('newCatSelect').value || 'Other',
|
category: document.getElementById('newCatSelect').value || 'Other',
|
||||||
status: document.getElementById('newStatus').value,
|
status: document.getElementById('newStatus').value,
|
||||||
};
|
};
|
||||||
|
// F2: Auto-populate address from GPS
|
||||||
|
if (AppState.gpsLat) {
|
||||||
|
const geo = await reverseGeocode(AppState.gpsLat, AppState.gpsLng);
|
||||||
|
if (geo) {
|
||||||
|
payload.address = geo.address || geo.formatted || '';
|
||||||
|
payload.building_number = geo.building_number || '';
|
||||||
|
payload.building_name = geo.building_name || '';
|
||||||
|
payload.trailer_number = geo.building_number || ''; // F4
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const asset = await api('/api/assets', {
|
const asset = await api('/api/assets', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -2819,6 +2839,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Reverse geocode GPS → address (Nominatim) ─────────────────────────
|
||||||
|
async function reverseGeocode(lat, lng) {
|
||||||
|
if (!lat || !lng) return null;
|
||||||
|
try {
|
||||||
|
const data = await api(`/api/geocode?lat=${lat}&lng=${lng}`);
|
||||||
|
return data; // { address, building_number, building_name, ... }
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Reverse geocode failed:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Populate address fields from geocode result ────────────────────────
|
||||||
|
function applyGeocodeToFields(geo, prefix = 'man') {
|
||||||
|
if (!geo) return;
|
||||||
|
const set = (suffix, val) => {
|
||||||
|
const el = document.getElementById(prefix + suffix);
|
||||||
|
if (el && !el.value) el.value = val || '';
|
||||||
|
};
|
||||||
|
set('Address', geo.address || geo.formatted || '');
|
||||||
|
set('BuildingNumber', geo.building_number || '');
|
||||||
|
set('BuildingName', geo.building_name || '');
|
||||||
|
set('TrailerNumber', geo.building_number || ''); // F4: sync building# → trailer
|
||||||
|
// Don't overwrite floor/room — those are interior details GPS can't know
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// OCR MODE
|
// OCR MODE
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
@@ -2918,15 +2964,26 @@
|
|||||||
showToast('Machine ID and name are required', true);
|
showToast('Machine ID and name are required', true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const payload = {
|
||||||
|
machine_id: machineId, name,
|
||||||
|
category: document.getElementById('ocrCatSelect').value || 'Other',
|
||||||
|
status: document.getElementById('ocrStatus').value,
|
||||||
|
};
|
||||||
|
// F2: Auto-populate address from GPS
|
||||||
|
if (AppState.gpsLat) {
|
||||||
|
const geo = await reverseGeocode(AppState.gpsLat, AppState.gpsLng);
|
||||||
|
if (geo) {
|
||||||
|
payload.address = geo.address || geo.formatted || '';
|
||||||
|
payload.building_number = geo.building_number || '';
|
||||||
|
payload.building_name = geo.building_name || '';
|
||||||
|
payload.trailer_number = geo.building_number || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const asset = await api('/api/assets', {
|
const asset = await api('/api/assets', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(payload),
|
||||||
machine_id: machineId, name,
|
|
||||||
category: document.getElementById('ocrCatSelect').value || 'Other',
|
|
||||||
status: document.getElementById('ocrStatus').value,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
showToast('Asset created!');
|
showToast('Asset created!');
|
||||||
document.getElementById('ocrCreateCard').style.display = 'none';
|
document.getElementById('ocrCreateCard').style.display = 'none';
|
||||||
@@ -3133,6 +3190,15 @@
|
|||||||
else showToast('Enter a map link first', true);
|
else showToast('Enter a map link first', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fillGpsParking(inputId) {
|
||||||
|
if (!AppState.gpsLat) {
|
||||||
|
showToast('GPS not available — allow location access', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById(inputId).value = AppState.gpsLat.toFixed(6) + ', ' + AppState.gpsLng.toFixed(6);
|
||||||
|
showToast('GPS coords filled!');
|
||||||
|
}
|
||||||
|
|
||||||
// Settings are loaded after auth (in doLogin / initAuth)
|
// Settings are loaded after auth (in doLogin / initAuth)
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
@@ -3524,7 +3590,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<input id="editWalkingDirections" type="text" class="input-field" placeholder="Walking directions" value="${esc(a.walking_directions || '')}">
|
<input id="editWalkingDirections" type="text" class="input-field" placeholder="Walking directions" value="${esc(a.walking_directions || '')}">
|
||||||
<input id="editMapLink" type="text" class="input-field" placeholder="Map link (URL)" value="${esc(a.map_link || '')}">
|
<input id="editMapLink" type="text" class="input-field" placeholder="Map link (URL)" value="${esc(a.map_link || '')}">
|
||||||
<input id="editParking" type="text" class="input-field" placeholder="Parking location" value="${esc(a.parking_location || '')}">
|
<div class="form-row">
|
||||||
|
<input id="editParking" type="text" class="input-field" placeholder="Parking location" value="${esc(a.parking_location || '')}">
|
||||||
|
<button class="btn btn-outline btn-sm" onclick="fillGpsParking('editParking')" style="flex-shrink:0;">📍 GPS</button>
|
||||||
|
</div>
|
||||||
<datalist id="catList">
|
<datalist id="catList">
|
||||||
<option value="Coffee"><option value="Cold Beverage"><option value="Snacks">
|
<option value="Coffee"><option value="Cold Beverage"><option value="Snacks">
|
||||||
<option value="Cold Food"><option value="Market"><option value="Smart Cooler">
|
<option value="Cold Food"><option value="Market"><option value="Smart Cooler">
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
Reference in New Issue
Block a user