06/11 Update Inspection result will include GPS

This commit is contained in:
2026-06-11 13:57:41 -04:00
parent 99546f4ed7
commit 079e826a7a
6 changed files with 98 additions and 2 deletions
+3 -1
View File
@@ -64,7 +64,9 @@ class Inspection(db.Model):
notes = db.Column(db.Text) # inspector free-text notes
form_data = db.Column(db.JSON) # filled form field responses {field_id: value}
completed_at = db.Column(db.DateTime)
mobile_local_id = db.Column(db.String(64), nullable=True, index=True) # idempotency key for mobile submissions
mobile_local_id = db.Column(db.String(64), nullable=True, index=True)
submit_latitude = db.Column(db.Numeric(10, 7), nullable=True)
submit_longitude = db.Column(db.Numeric(10, 7), nullable=True)
# ── Re-inspection / follow-up workflow ────────────────────────────────
parent_inspection_id = db.Column(
+10
View File
@@ -533,6 +533,16 @@ def execute(inspection_id):
inspection.status = 'completed'
inspection.completed_at = now_eastern()
# Capture GPS coordinates submitted by the browser Geolocation API
try:
_lat = request.form.get('submit_lat', '').strip()
_lng = request.form.get('submit_lng', '').strip()
if _lat and _lng:
inspection.submit_latitude = float(_lat)
inspection.submit_longitude = float(_lng)
except (ValueError, TypeError):
pass
# Snapshot the current template schema so view() renders correctly
# even if the template is later edited or deleted.
_save_responses(inspection, responses, snapshot_schema=form_fields)
+19
View File
@@ -238,6 +238,8 @@
<div class="insp-wrap mt-3">
<form method="post" action="{{ url_for('inspections.execute', inspection_id=inspection.id) }}" enctype="multipart/form-data" id="inspectionForm" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="submit_lat" id="submitLat">
<input type="hidden" name="submit_lng" id="submitLng">
{# ── Header ── #}
<div class="insp-header">
@@ -889,6 +891,23 @@ document.getElementById('inspectionForm').addEventListener('submit', async funct
}
}
}
// Capture GPS on final submit (not draft) — fire-and-forget with short timeout
if (_submittingAction === 'submit' && navigator.geolocation) {
await new Promise(resolve => {
const timer = setTimeout(resolve, 4000); // give up after 4 s
navigator.geolocation.getCurrentPosition(
pos => {
document.getElementById('submitLat').value = pos.coords.latitude;
document.getElementById('submitLng').value = pos.coords.longitude;
clearTimeout(timer);
resolve();
},
() => { clearTimeout(timer); resolve(); }, // permission denied / unavailable — proceed without
{ timeout: 4000, maximumAge: 60000 }
);
});
}
// Native form submit — browser handles multipart encoding and follows the redirect
form.submit();
} catch (err) {
+26
View File
@@ -510,6 +510,32 @@
</div>
</div>
{# ── Submission GPS (admin / director only) ──────────────────────────── #}
{% if current_user.role in ['admin', 'director'] and inspection.submit_latitude and inspection.submit_longitude %}
{% set _lat = inspection.submit_latitude | float %}
{% set _lng = inspection.submit_longitude | float %}
{% set _maps_key = config.get('GOOGLE_MAPS_API_KEY', '') %}
<div class="mb-4">
<div class="lbl mb-1" style="font-size:.72rem;color:#94a3b8;font-weight:500;text-transform:uppercase;letter-spacing:.04em;">
<i class="bi bi-geo-alt-fill text-danger me-1"></i>Submission Location
</div>
{% if _maps_key %}
<div style="border-radius:10px;overflow:hidden;border:1px solid #e2e8f0;max-width:420px;">
<img src="https://maps.googleapis.com/maps/api/staticmap?center={{ _lat }},{{ _lng }}&zoom=15&size=420x220&maptype=roadmap&markers=color:red%7C{{ _lat }},{{ _lng }}&key={{ _maps_key }}"
alt="Submission location map"
style="width:100%;display:block;">
</div>
{% endif %}
<div class="mt-1" style="font-size:.8rem;color:#64748b;">
{{ '%.6f' | format(_lat) }}, {{ '%.6f' | format(_lng) }}
<a href="https://www.google.com/maps?q={{ _lat }},{{ _lng }}" target="_blank"
class="ms-2 text-decoration-none small">
<i class="bi bi-box-arrow-up-right"></i> Open in Maps
</a>
</div>
</div>
{% endif %}
{# ── Build the filtered field set ──────────────────────────────────────
Strategy:
1. Find all grid rows that contain at least one rated rating field.
+3
View File
@@ -51,6 +51,9 @@ class Config:
# ── Digest email secret token (used to authenticate cron trigger) ────────
DIGEST_SECRET = os.environ.get('DIGEST_SECRET')
# ── Google Maps (used for GPS map on inspection view) ────────────────────
GOOGLE_MAPS_API_KEY = os.environ.get('GOOGLE_MAPS_API_KEY', '')
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
@@ -0,0 +1,36 @@
"""phase25 — add GPS coordinates to inspections
Adds submit_latitude and submit_longitude columns to the inspections table.
Populated at web submission time via browser Geolocation API.
Null for all existing inspections and mobile submissions (handled separately).
"""
import sqlalchemy as sa
from alembic import op
revision = 'phase25_inspection_gps'
down_revision = 'phase24_notify_defaults'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
has_lat = conn.execute(sa.text(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS "
"WHERE TABLE_SCHEMA = DATABASE() "
" AND TABLE_NAME = 'inspections' "
" AND COLUMN_NAME = 'submit_latitude'"
)).scalar()
if not has_lat:
op.add_column('inspections',
sa.Column('submit_latitude', sa.Numeric(10, 7), nullable=True))
op.add_column('inspections',
sa.Column('submit_longitude', sa.Numeric(10, 7), nullable=True))
def downgrade():
op.drop_column('inspections', 'submit_longitude')
op.drop_column('inspections', 'submit_latitude')