From 0bbda152af3804b5ff0b1bb23486f6411e35ecf5 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Fri, 27 Feb 2026 14:53:40 -0500 Subject: [PATCH] Feb 27 2026: fix iPad UI --- app/routes/inspections.py | 27 ++- app/static/css/ipad_responsive.css | 22 ++- app/templates/inspections/execute.html | 251 ++++++++++++++++++------- 3 files changed, 219 insertions(+), 81 deletions(-) diff --git a/app/routes/inspections.py b/app/routes/inspections.py index d953470..5f814e5 100644 --- a/app/routes/inspections.py +++ b/app/routes/inspections.py @@ -39,12 +39,18 @@ def _save_photo(file_obj, subfolder='inspection_photos'): return f"uploads/{subfolder}/{filename}" -def _collect_form_responses(form_fields): +def _collect_form_responses(form_fields, existing_responses=None): """ Walk the submitted form data and collect responses keyed by field ID. Returns a dict: { field_id: value_or_list_or_path } Photo uploads are saved to disk; their path is stored as the value. + + existing_responses: previously saved form data (from inspection.notes). + Used to preserve photo paths when no new file is uploaded on resubmit. """ + if existing_responses is None: + existing_responses = {} + responses = {} for field in form_fields: fid = field['id'] @@ -64,7 +70,18 @@ def _collect_form_responses(form_fields): elif ftype == 'image': photo_file = request.files.get(key) path = _save_photo(photo_file, subfolder='inspection_photos') - responses[fid] = path or '' + if path: + # New file uploaded — use the new path + responses[fid] = path + else: + # No new file — preserve the previously saved photo path. + # JSON keys are always strings; try both str and original type. + existing_path = ( + existing_responses.get(str(fid)) + or existing_responses.get(fid) + or '' + ) + responses[fid] = existing_path elif ftype == 'table': cols = field.get('col_headers') or ['Column 1'] @@ -261,8 +278,10 @@ def execute(inspection_id): if request.method == 'POST': action = request.form.get('action', 'submit') - # Collect all field responses from the submitted form - responses = _collect_form_responses(form_fields) + # Collect all field responses from the submitted form. + # Pass saved_responses so existing photo paths are preserved + # when no new file is selected on this submission. + responses = _collect_form_responses(form_fields, saved_responses) if action == 'submit': # Validate required fields diff --git a/app/static/css/ipad_responsive.css b/app/static/css/ipad_responsive.css index 1c3bb3f..dc319dd 100644 --- a/app/static/css/ipad_responsive.css +++ b/app/static/css/ipad_responsive.css @@ -235,18 +235,26 @@ } } -/* ── 12. Rating stars — ensure tap targets ── */ +/* ── 12. Rating stars — tap targets sized to grid cell, not oversized ── */ @media (pointer: coarse) { + /* Inside the inspection form grid, stars must stay compact to fit their cell. + The grid cell height is ~50px; 1.6rem stars at 44px each would overflow. */ .rating-stars button { - font-size: 1.6rem !important; - padding: 0.25rem !important; - min-height: 44px !important; - min-width: 44px !important; + font-size: 1rem; /* fits within grid cell rows */ + padding: 0.15rem; + min-height: unset; /* do not enforce 44px — cell height controls this */ + min-width: unset; } - /* Upload zone — larger tap area */ + /* Outside the grid (e.g. standalone forms), keep comfortable tap targets */ + :not(.form-grid) .rating-stars button { + font-size: 1.4rem; + padding: 0.2rem; + } + + /* Upload zone — compact inside grid, comfortable outside */ .upload-zone { - min-height: 80px !important; + min-height: 44px; /* reduced from 80px — fits grid cell */ } /* Signature pad — taller for finger drawing */ diff --git a/app/templates/inspections/execute.html b/app/templates/inspections/execute.html index 8eb8a7d..6098b0c 100644 --- a/app/templates/inspections/execute.html +++ b/app/templates/inspections/execute.html @@ -42,14 +42,43 @@ --_gap: 8px; --_cell: calc((min(calc(100vw - 2rem), 900px) - 11 * 8px) / 12); grid-template-columns: repeat(12, var(--_cell)); - grid-auto-rows: calc(var(--_cell) * 0.72); + /* Use auto rows so image/upload cells can expand to their content */ + grid-auto-rows: auto; width: 100%; + /* Minimum row height keeps non-image rows from collapsing */ + grid-auto-rows: minmax(calc(var(--_cell) * 0.72), auto); } .insp-body { padding: 1rem; } + + /* Rating stars: scale down to fit compressed grid cells on iPad */ + .rating-stars button { font-size: .85rem; } + + /* Image cells: allow full content height, don't clip the clear button */ + .fg-cell:has(.upload-wrap) { + overflow: visible; + height: auto; + } + /* upload-wrap must not be clipped — let it grow */ + .upload-wrap { + flex: none; + } + /* upload-zone fixed compact height on iPad so it doesn't over-expand */ + .upload-zone { + min-height: 44px; + flex: none; + height: 44px; + } + /* Always show the clear button text without clipping */ + .upload-clear { + white-space: nowrap; + overflow: visible; + } } .fg-cell { overflow:hidden; display:flex; flex-direction:column; padding:.18rem .45rem; } + /* Image upload cells need overflow visible so the remove button isn't clipped */ + .fg-cell:has(.upload-wrap) { overflow: visible; } .fg-cell .field-lbl { font-size:.74rem; font-weight:500; color:#64748b; margin-bottom:.2rem; display:block; @@ -71,18 +100,34 @@ .fg-cell .form-check-input { margin-top:.18rem; } .fg-cell .form-check { margin-bottom:.1rem; } + /* Upload zone — uses