Feb 27 2026: fix iPad UI
This commit is contained in:
@@ -39,12 +39,18 @@ def _save_photo(file_obj, subfolder='inspection_photos'):
|
|||||||
return f"uploads/{subfolder}/{filename}"
|
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.
|
Walk the submitted form data and collect responses keyed by field ID.
|
||||||
Returns a dict: { field_id: value_or_list_or_path }
|
Returns a dict: { field_id: value_or_list_or_path }
|
||||||
Photo uploads are saved to disk; their path is stored as the value.
|
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 = {}
|
responses = {}
|
||||||
for field in form_fields:
|
for field in form_fields:
|
||||||
fid = field['id']
|
fid = field['id']
|
||||||
@@ -64,7 +70,18 @@ def _collect_form_responses(form_fields):
|
|||||||
elif ftype == 'image':
|
elif ftype == 'image':
|
||||||
photo_file = request.files.get(key)
|
photo_file = request.files.get(key)
|
||||||
path = _save_photo(photo_file, subfolder='inspection_photos')
|
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':
|
elif ftype == 'table':
|
||||||
cols = field.get('col_headers') or ['Column 1']
|
cols = field.get('col_headers') or ['Column 1']
|
||||||
@@ -261,8 +278,10 @@ def execute(inspection_id):
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
action = request.form.get('action', 'submit')
|
action = request.form.get('action', 'submit')
|
||||||
|
|
||||||
# Collect all field responses from the submitted form
|
# Collect all field responses from the submitted form.
|
||||||
responses = _collect_form_responses(form_fields)
|
# 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':
|
if action == 'submit':
|
||||||
# Validate required fields
|
# Validate required fields
|
||||||
|
|||||||
@@ -235,18 +235,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 12. Rating stars — ensure tap targets ── */
|
/* ── 12. Rating stars — tap targets sized to grid cell, not oversized ── */
|
||||||
@media (pointer: coarse) {
|
@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 {
|
.rating-stars button {
|
||||||
font-size: 1.6rem !important;
|
font-size: 1rem; /* fits within grid cell rows */
|
||||||
padding: 0.25rem !important;
|
padding: 0.15rem;
|
||||||
min-height: 44px !important;
|
min-height: unset; /* do not enforce 44px — cell height controls this */
|
||||||
min-width: 44px !important;
|
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 {
|
.upload-zone {
|
||||||
min-height: 80px !important;
|
min-height: 44px; /* reduced from 80px — fits grid cell */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Signature pad — taller for finger drawing */
|
/* Signature pad — taller for finger drawing */
|
||||||
|
|||||||
@@ -42,14 +42,43 @@
|
|||||||
--_gap: 8px;
|
--_gap: 8px;
|
||||||
--_cell: calc((min(calc(100vw - 2rem), 900px) - 11 * 8px) / 12);
|
--_cell: calc((min(calc(100vw - 2rem), 900px) - 11 * 8px) / 12);
|
||||||
grid-template-columns: repeat(12, var(--_cell));
|
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%;
|
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; }
|
.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 {
|
.fg-cell {
|
||||||
overflow:hidden; display:flex; flex-direction:column; padding:.18rem .45rem;
|
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 {
|
.fg-cell .field-lbl {
|
||||||
font-size:.74rem; font-weight:500; color:#64748b;
|
font-size:.74rem; font-weight:500; color:#64748b;
|
||||||
margin-bottom:.2rem; display:block;
|
margin-bottom:.2rem; display:block;
|
||||||
@@ -71,18 +100,34 @@
|
|||||||
.fg-cell .form-check-input { margin-top:.18rem; }
|
.fg-cell .form-check-input { margin-top:.18rem; }
|
||||||
.fg-cell .form-check { margin-bottom:.1rem; }
|
.fg-cell .form-check { margin-bottom:.1rem; }
|
||||||
|
|
||||||
|
/* Upload zone — uses <label> wrapper so iOS Safari reliably opens file picker */
|
||||||
.upload-zone {
|
.upload-zone {
|
||||||
border:2px dashed #cbd5e1; border-radius:6px; flex:1; min-height:0;
|
border:2px dashed #cbd5e1; border-radius:6px; flex:1; min-height:36px;
|
||||||
display:flex; flex-direction:column; align-items:center; justify-content:center;
|
display:flex; flex-direction:column; align-items:center; justify-content:center;
|
||||||
color:#64748b; background:#f8fafc; cursor:pointer; font-size:.74rem; gap:.2rem;
|
color:#64748b; background:#f8fafc; cursor:pointer; font-size:.74rem; gap:.2rem;
|
||||||
transition:border-color .18s, background .18s; position:relative;
|
transition:border-color .18s, background .18s;
|
||||||
|
-webkit-tap-highlight-color: rgba(37,99,235,.08);
|
||||||
}
|
}
|
||||||
.upload-zone:hover { border-color:#2563eb; background:#eff6ff; color:#2563eb; }
|
.upload-zone:hover,
|
||||||
|
.upload-zone:active { border-color:#2563eb; background:#eff6ff; color:#2563eb; }
|
||||||
|
/* The actual file input is visually hidden but accessible — NOT opacity:0/absolute
|
||||||
|
because iOS treats that as non-interactive. Instead we use clip + size trick. */
|
||||||
.upload-zone input[type=file] {
|
.upload-zone input[type=file] {
|
||||||
position:absolute; inset:0; opacity:0; cursor:pointer; width:100%; height:100%;
|
position:absolute; width:1px; height:1px; overflow:hidden;
|
||||||
|
clip:rect(0,0,0,0); white-space:nowrap;
|
||||||
}
|
}
|
||||||
.upload-zone i { font-size:1.1rem; }
|
.upload-zone i { font-size:1.1rem; pointer-events:none; }
|
||||||
.upload-zone .file-name { font-size:.7rem; color:#2563eb; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
.upload-zone .upload-prompt { pointer-events:none; }
|
||||||
|
.upload-zone .file-name { font-size:.7rem; color:#2563eb; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; pointer-events:none; }
|
||||||
|
|
||||||
|
/* Fix 2: clear button for selected photo */
|
||||||
|
.upload-clear {
|
||||||
|
display:none; font-size:.68rem; padding:.1rem .35rem;
|
||||||
|
background:#fee2e2; border:1px solid #fca5a5; border-radius:4px;
|
||||||
|
color:#dc2626; cursor:pointer; margin-top:.15rem; line-height:1.4;
|
||||||
|
}
|
||||||
|
.upload-clear:hover { background:#fecaca; }
|
||||||
|
.upload-wrap { display:flex; flex-direction:column; flex:1; min-height:0; }
|
||||||
|
|
||||||
.signature-pad-wrap {
|
.signature-pad-wrap {
|
||||||
flex:1; min-height:0; border-radius:6px; border:1px solid #e2e8f0;
|
flex:1; min-height:0; border-radius:6px; border:1px solid #e2e8f0;
|
||||||
@@ -167,7 +212,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="insp-wrap mt-3">
|
<div class="insp-wrap mt-3">
|
||||||
<form method="post" 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() }}">
|
||||||
|
|
||||||
{# ── Header ── #}
|
{# ── Header ── #}
|
||||||
@@ -322,17 +367,27 @@
|
|||||||
{# ── Image / Photo upload ── #}
|
{# ── Image / Photo upload ── #}
|
||||||
{% elif field.type == 'image' %}
|
{% elif field.type == 'image' %}
|
||||||
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
|
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
|
||||||
{% if saved %}
|
<div class="upload-wrap">
|
||||||
<img src="{{ url_for('static', filename=saved) }}" class="photo-thumb" alt="Uploaded photo">
|
{% if saved %}
|
||||||
{% endif %}
|
<img src="{{ url_for('static', filename=saved) }}" class="photo-thumb" id="thumb_{{ fid }}" alt="Uploaded photo">
|
||||||
<div class="upload-zone">
|
{% else %}
|
||||||
<input type="file" name="field_{{ fid }}" accept="image/*"
|
<img class="photo-thumb" id="thumb_{{ fid }}" alt="Preview" style="display:none; max-height:60px; border-radius:4px; margin-bottom:.25rem;">
|
||||||
onchange="showFileName(this)">
|
{% endif %}
|
||||||
<i class="bi bi-cloud-upload"></i>
|
{# <label> wrapper is required for reliable iOS file picker activation #}
|
||||||
<span>Click or drag to upload</span>
|
<label class="upload-zone" for="field_{{ fid }}" id="zone_{{ fid }}">
|
||||||
<span class="file-name" id="fname_{{ fid }}">
|
<input type="file" name="field_{{ fid }}" id="field_{{ fid }}" accept="image/*"
|
||||||
{% if saved %}{{ saved.split('/')[-1] }}{% endif %}
|
onchange="showFileName(this)">
|
||||||
</span>
|
<i class="bi bi-cloud-upload"></i>
|
||||||
|
<span class="upload-prompt">Tap to take / choose photo</span>
|
||||||
|
<span class="file-name" id="fname_{{ fid }}">
|
||||||
|
{% if saved %}{{ saved.split('/')[-1] }}{% endif %}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<button type="button" class="upload-clear" id="clear_{{ fid }}"
|
||||||
|
onclick="clearUpload('{{ fid }}')"
|
||||||
|
{% if saved %}style="display:block"{% endif %}>
|
||||||
|
<i class="bi bi-x-circle"></i> Remove photo
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
|
{% if field.help_text %}<div class="help-text">{{ field.help_text }}</div>{% endif %}
|
||||||
|
|
||||||
@@ -398,8 +453,7 @@
|
|||||||
<button type="submit" name="action" value="draft" class="btn btn-sm btn-outline-light">
|
<button type="submit" name="action" value="draft" class="btn btn-sm btn-outline-light">
|
||||||
<i class="bi bi-floppy"></i> Save Draft
|
<i class="bi bi-floppy"></i> Save Draft
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="action" value="submit" class="btn btn-success"
|
<button type="submit" name="action" value="submit" class="btn btn-success">
|
||||||
onclick="collectSignatures(); return confirmSubmit();">
|
|
||||||
<i class="bi bi-check-circle-fill"></i> Submit Inspection
|
<i class="bi bi-check-circle-fill"></i> Submit Inspection
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -423,15 +477,55 @@ function setRating(btn) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── File upload label ─────────────────────────────────────────────────────────
|
// ── File upload: show filename, preview thumbnail, enable clear button ──────────
|
||||||
function showFileName(input) {
|
function showFileName(input) {
|
||||||
const wrap = input.closest('.upload-zone');
|
const fid = input.id.replace('field_', '');
|
||||||
const label = wrap ? wrap.querySelector('.file-name') : null;
|
const label = document.getElementById('fname_' + fid);
|
||||||
if (label && input.files.length) {
|
const thumb = document.getElementById('thumb_' + fid);
|
||||||
label.textContent = input.files[0].name;
|
const clear = document.getElementById('clear_' + fid);
|
||||||
|
|
||||||
|
if (input.files && input.files.length > 0) {
|
||||||
|
const file = input.files[0];
|
||||||
|
|
||||||
|
// Show filename
|
||||||
|
if (label) label.textContent = file.name;
|
||||||
|
|
||||||
|
// Show image preview via FileReader
|
||||||
|
if (thumb && file.type.startsWith('image/')) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
thumb.src = e.target.result;
|
||||||
|
thumb.style.display = 'block';
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the clear button
|
||||||
|
if (clear) clear.style.display = 'block';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Clear a photo upload field ────────────────────────────────────────────────
|
||||||
|
// On iOS, input.files is a read-only FileList — setting input.value='' does NOT
|
||||||
|
// clear it. The only reliable cross-browser reset is to replace the input element.
|
||||||
|
function clearUpload(fid) {
|
||||||
|
const input = document.getElementById('field_' + fid);
|
||||||
|
const label = document.getElementById('fname_' + fid);
|
||||||
|
const thumb = document.getElementById('thumb_' + fid);
|
||||||
|
const clear = document.getElementById('clear_' + fid);
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
// Replace the file input with a fresh clone — the only reliable iOS reset
|
||||||
|
const newInput = input.cloneNode(false); // false = no children
|
||||||
|
newInput.value = '';
|
||||||
|
newInput.addEventListener('change', function() { showFileName(this); });
|
||||||
|
input.parentNode.replaceChild(newInput, input);
|
||||||
|
}
|
||||||
|
if (label) label.textContent = '';
|
||||||
|
if (thumb) { thumb.src = ''; thumb.style.display = 'none'; }
|
||||||
|
if (clear) clear.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
// ── Signature pads ────────────────────────────────────────────────────────────
|
// ── Signature pads ────────────────────────────────────────────────────────────
|
||||||
const sigPads = {};
|
const sigPads = {};
|
||||||
document.querySelectorAll('[id^="sig_"]').forEach(canvas => {
|
document.querySelectorAll('[id^="sig_"]').forEach(canvas => {
|
||||||
@@ -492,11 +586,6 @@ function collectSignatures() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Submit confirmation ───────────────────────────────────────────────────────
|
|
||||||
function confirmSubmit() {
|
|
||||||
return confirm('Submit this inspection? This action cannot be undone.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Client-side image compression ─────────────────────────────────────────────
|
// ── Client-side image compression ─────────────────────────────────────────────
|
||||||
// Compresses photos via Canvas before upload to prevent 413 errors.
|
// Compresses photos via Canvas before upload to prevent 413 errors.
|
||||||
// Target: ≤ 1MB per image at 85% JPEG quality, max 1920px on longest side.
|
// Target: ≤ 1MB per image at 85% JPEG quality, max 1920px on longest side.
|
||||||
@@ -544,54 +633,76 @@ function compressImageFile(file) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intercept form submission — compress all image fields before sending
|
// ── Form submission: compress images then submit natively ────────────────────
|
||||||
|
// Using native form POST (not fetch) avoids all iOS Safari redirect/fetch quirks.
|
||||||
|
// Images are compressed via Canvas → DataTransfer → file input replacement before submit.
|
||||||
|
|
||||||
|
// Track which button was clicked (submit vs draft)
|
||||||
|
let _submittingAction = 'submit';
|
||||||
|
document.querySelectorAll('#inspectionForm button[type="submit"]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
_submittingAction = this.value || 'submit';
|
||||||
|
}, true); // capture phase — fires before submit event
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('inspectionForm').addEventListener('submit', async function(e) {
|
document.getElementById('inspectionForm').addEventListener('submit', async function(e) {
|
||||||
const fileInputs = this.querySelectorAll('input[type="file"]');
|
|
||||||
if (!fileInputs.length) return; // nothing to compress
|
|
||||||
|
|
||||||
// Only intercept if there are files to compress
|
|
||||||
const hasFiles = Array.from(fileInputs).some(inp => inp.files && inp.files.length > 0);
|
|
||||||
if (!hasFiles) return;
|
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const btn = this.querySelector('button[type="submit"]');
|
// Confirm before final submission (not for drafts)
|
||||||
const origText = btn ? btn.innerHTML : '';
|
if (_submittingAction === 'submit') {
|
||||||
if (btn) {
|
if (!confirm('Submit this inspection? This action cannot be undone.')) return;
|
||||||
btn.disabled = true;
|
|
||||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Compressing photos…';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
collectSignatures(); // Flush signature canvases to hidden inputs
|
||||||
|
|
||||||
|
const form = this;
|
||||||
|
const fileInputs = Array.from(form.querySelectorAll('input[type="file"]'))
|
||||||
|
.filter(inp => inp.files && inp.files.length > 0);
|
||||||
|
|
||||||
|
// Show spinner on the active button
|
||||||
|
const activeBtn = Array.from(form.querySelectorAll('button[type="submit"]'))
|
||||||
|
.find(b => b.value === _submittingAction)
|
||||||
|
|| form.querySelector('button[type="submit"]');
|
||||||
|
const origHTML = activeBtn ? activeBtn.innerHTML : '';
|
||||||
|
form.querySelectorAll('button[type="submit"]').forEach(b => b.disabled = true);
|
||||||
|
if (activeBtn) {
|
||||||
|
activeBtn.innerHTML = fileInputs.length
|
||||||
|
? '<span class="spinner-border spinner-border-sm me-1"></span> Uploading…'
|
||||||
|
: '<span class="spinner-border spinner-border-sm me-1"></span> Saving…';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the action value into a hidden field so the native POST includes it
|
||||||
|
let actionInput = form.querySelector('input[name="action"]');
|
||||||
|
if (!actionInput) {
|
||||||
|
actionInput = document.createElement('input');
|
||||||
|
actionInput.type = 'hidden';
|
||||||
|
actionInput.name = 'action';
|
||||||
|
form.appendChild(actionInput);
|
||||||
|
}
|
||||||
|
actionInput.value = _submittingAction;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build a FormData object with compressed images
|
// Compress images and replace file input contents via DataTransfer
|
||||||
const formData = new FormData(this);
|
|
||||||
|
|
||||||
for (const input of fileInputs) {
|
for (const input of fileInputs) {
|
||||||
if (!input.files || !input.files.length) continue;
|
|
||||||
const compressed = await compressImageFile(input.files[0]);
|
const compressed = await compressImageFile(input.files[0]);
|
||||||
formData.set(input.name, compressed, compressed.name);
|
// DataTransfer lets us programmatically set input.files (iOS 15.4+, all modern browsers)
|
||||||
}
|
if (typeof DataTransfer !== 'undefined') {
|
||||||
|
try {
|
||||||
// Submit via fetch with the compressed FormData
|
const dt = new DataTransfer();
|
||||||
const response = await fetch(this.action || window.location.href, {
|
dt.items.add(compressed);
|
||||||
method: 'POST',
|
input.files = dt.files;
|
||||||
body: formData,
|
} catch (_) {
|
||||||
credentials: 'same-origin',
|
// DataTransfer.items not supported — submit original file uncompressed
|
||||||
});
|
}
|
||||||
|
}
|
||||||
if (response.redirected) {
|
|
||||||
window.location.href = response.url;
|
|
||||||
} else if (response.ok) {
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
// Surface server error (e.g. still-too-large after compression)
|
|
||||||
if (btn) { btn.disabled = false; btn.innerHTML = origText; }
|
|
||||||
alert('Submission failed (HTTP ' + response.status + '). Please try again or remove some photos.');
|
|
||||||
}
|
}
|
||||||
|
// Native form submit — browser handles multipart encoding and follows the redirect
|
||||||
|
form.submit();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (btn) { btn.disabled = false; btn.innerHTML = origText; }
|
form.querySelectorAll('button[type="submit"]').forEach(b => b.disabled = false);
|
||||||
console.error('Photo compression error:', err);
|
if (activeBtn) activeBtn.innerHTML = origHTML;
|
||||||
alert('An error occurred while processing photos. Please try again.');
|
console.error('Inspection submit error:', err);
|
||||||
|
alert('An error occurred. Please try again.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user