06/11 Update Inspection result will include GPS
This commit is contained in:
@@ -63,8 +63,10 @@ class Inspection(db.Model):
|
|||||||
status = db.Column(db.Enum('in_progress', 'completed', 'flagged'), default='in_progress')
|
status = db.Column(db.Enum('in_progress', 'completed', 'flagged'), default='in_progress')
|
||||||
notes = db.Column(db.Text) # inspector free-text notes
|
notes = db.Column(db.Text) # inspector free-text notes
|
||||||
form_data = db.Column(db.JSON) # filled form field responses {field_id: value}
|
form_data = db.Column(db.JSON) # filled form field responses {field_id: value}
|
||||||
completed_at = db.Column(db.DateTime)
|
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 ────────────────────────────────
|
# ── Re-inspection / follow-up workflow ────────────────────────────────
|
||||||
parent_inspection_id = db.Column(
|
parent_inspection_id = db.Column(
|
||||||
|
|||||||
@@ -533,6 +533,16 @@ def execute(inspection_id):
|
|||||||
inspection.status = 'completed'
|
inspection.status = 'completed'
|
||||||
inspection.completed_at = now_eastern()
|
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
|
# Snapshot the current template schema so view() renders correctly
|
||||||
# even if the template is later edited or deleted.
|
# even if the template is later edited or deleted.
|
||||||
_save_responses(inspection, responses, snapshot_schema=form_fields)
|
_save_responses(inspection, responses, snapshot_schema=form_fields)
|
||||||
|
|||||||
@@ -238,6 +238,8 @@
|
|||||||
<div class="insp-wrap mt-3">
|
<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>
|
<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="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="submit_lat" id="submitLat">
|
||||||
|
<input type="hidden" name="submit_lng" id="submitLng">
|
||||||
|
|
||||||
{# ── Header ── #}
|
{# ── Header ── #}
|
||||||
<div class="insp-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
|
// Native form submit — browser handles multipart encoding and follows the redirect
|
||||||
form.submit();
|
form.submit();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -510,6 +510,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 ──────────────────────────────────────
|
{# ── Build the filtered field set ──────────────────────────────────────
|
||||||
Strategy:
|
Strategy:
|
||||||
1. Find all grid rows that contain at least one rated rating field.
|
1. Find all grid rows that contain at least one rated rating field.
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ class Config:
|
|||||||
# ── Digest email secret token (used to authenticate cron trigger) ────────
|
# ── Digest email secret token (used to authenticate cron trigger) ────────
|
||||||
DIGEST_SECRET = os.environ.get('DIGEST_SECRET')
|
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_SERVER = os.environ.get('MAIL_SERVER')
|
||||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
||||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
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')
|
||||||
Reference in New Issue
Block a user