05/25 Fix lost inspection's photos after flagging an issue
This commit is contained in:
@@ -413,13 +413,17 @@
|
||||
{% elif field.type == 'image' %}
|
||||
<label class="field-lbl">{{ field.label }}{% if field.required %}<span class="required-mark"> *</span>{% endif %}</label>
|
||||
{# Single <label> element: tap anywhere to pick a photo.
|
||||
Filename + remove button appear on the right when a file is chosen. #}
|
||||
Filename + remove button appear on the right when a file is chosen.
|
||||
The hidden server_path input stores the AJAX-uploaded path so it is
|
||||
included in the Flag-Issue draft save (file inputs can't be serialised). #}
|
||||
<label class="upload-zone" for="field_{{ fid }}" id="zone_{{ fid }}">
|
||||
<input type="file" name="field_{{ fid }}" id="field_{{ fid }}" accept="image/*"
|
||||
onchange="showFileName(this)">
|
||||
onchange="uploadPhotoField(this)"
|
||||
data-upload-url="{{ url_for('inspections.upload_photo_ajax', inspection_id=inspection.id) }}">
|
||||
<input type="hidden" id="field_{{ fid }}_server_path" value="{{ saved or '' }}">
|
||||
<div class="upload-main">
|
||||
<i class="bi bi-cloud-upload"></i>
|
||||
<span class="upload-prompt">Tap to take / choose photo</span>
|
||||
<i class="bi bi-cloud-upload" id="icon_{{ fid }}"></i>
|
||||
<span class="upload-prompt" id="prompt_{{ fid }}">Tap to take / choose photo</span>
|
||||
</div>
|
||||
<div class="upload-chosen{% if saved %} has-file{% endif %}" id="chosen_{{ fid }}">
|
||||
<span class="upload-fname" id="fname_{{ fid }}">{% if saved %}{{ saved.split('/')[-1] }}{% endif %}</span>
|
||||
@@ -560,33 +564,79 @@ function setRating(btn) {
|
||||
});
|
||||
}
|
||||
|
||||
// ── File upload: show filename + reveal the chosen panel ────────────────────
|
||||
function showFileName(input) {
|
||||
const fid = input.id.replace('field_', '');
|
||||
const fname = document.getElementById('fname_' + fid);
|
||||
const chosen = document.getElementById('chosen_' + fid);
|
||||
// ── Photo upload on select: immediately uploads to server ────────────────────
|
||||
// Stores the server path in a hidden field so the Flag-Issue AJAX draft save
|
||||
// can include it — file inputs cannot be JSON-serialised or sent in a JSON body.
|
||||
async function uploadPhotoField(input) {
|
||||
const fid = input.id.replace('field_', '');
|
||||
const fname = document.getElementById('fname_' + fid);
|
||||
const chosen = document.getElementById('chosen_' + fid);
|
||||
const pathHidden = document.getElementById('field_' + fid + '_server_path');
|
||||
const iconEl = document.getElementById('icon_' + fid);
|
||||
const promptEl = document.getElementById('prompt_' + fid);
|
||||
|
||||
if (input.files && input.files.length > 0) {
|
||||
if (fname) fname.textContent = input.files[0].name;
|
||||
if (chosen) chosen.classList.add('has-file');
|
||||
if (!input.files || !input.files.length) return;
|
||||
const file = input.files[0];
|
||||
|
||||
// Show the filename chip immediately (optimistic UI)
|
||||
if (fname) fname.textContent = file.name;
|
||||
if (chosen) chosen.classList.add('has-file');
|
||||
|
||||
// Show uploading state
|
||||
if (iconEl) iconEl.className = 'bi bi-hourglass-split';
|
||||
if (promptEl) promptEl.textContent = 'Uploading…';
|
||||
|
||||
try {
|
||||
const uploadUrl = input.dataset.uploadUrl;
|
||||
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||
|
||||
const compressed = await compressImageFile(file);
|
||||
const fd = new FormData();
|
||||
fd.append('photo', compressed, file.name);
|
||||
fd.append('csrf_token', csrfToken);
|
||||
|
||||
const res = await fetch(uploadUrl, { method: 'POST', body: fd });
|
||||
const json = await res.json();
|
||||
|
||||
if (json.ok && json.path) {
|
||||
if (pathHidden) pathHidden.value = json.path;
|
||||
if (iconEl) iconEl.className = 'bi bi-check-circle-fill';
|
||||
if (promptEl) promptEl.textContent = 'Photo saved';
|
||||
} else {
|
||||
// Upload failed — file is still in the input, will be sent on full form submit
|
||||
if (iconEl) iconEl.className = 'bi bi-cloud-upload';
|
||||
if (promptEl) promptEl.textContent = 'Tap to take / choose photo';
|
||||
console.error('Photo AJAX upload failed:', json.error);
|
||||
}
|
||||
} catch (err) {
|
||||
// Network error — same fallback: file stays in input for final form submit
|
||||
if (iconEl) iconEl.className = 'bi bi-cloud-upload';
|
||||
if (promptEl) promptEl.textContent = 'Tap to take / choose photo';
|
||||
console.error('Photo upload error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Clear a photo upload field ────────────────────────────────────────────────
|
||||
// iOS: input.files is read-only — replace the element to truly reset it.
|
||||
function clearUpload(fid) {
|
||||
const input = document.getElementById('field_' + fid);
|
||||
const fname = document.getElementById('fname_' + fid);
|
||||
const chosen = document.getElementById('chosen_' + fid);
|
||||
const input = document.getElementById('field_' + fid);
|
||||
const fname = document.getElementById('fname_' + fid);
|
||||
const chosen = document.getElementById('chosen_' + fid);
|
||||
const pathHidden = document.getElementById('field_' + fid + '_server_path');
|
||||
const iconEl = document.getElementById('icon_' + fid);
|
||||
const promptEl = document.getElementById('prompt_' + fid);
|
||||
|
||||
if (input) {
|
||||
const newInput = input.cloneNode(false);
|
||||
newInput.value = '';
|
||||
newInput.addEventListener('change', function() { showFileName(this); });
|
||||
newInput.addEventListener('change', function() { uploadPhotoField(this); });
|
||||
input.parentNode.replaceChild(newInput, input);
|
||||
}
|
||||
if (fname) fname.textContent = '';
|
||||
if (chosen) chosen.classList.remove('has-file');
|
||||
if (fname) fname.textContent = '';
|
||||
if (chosen) chosen.classList.remove('has-file');
|
||||
if (pathHidden) pathHidden.value = '';
|
||||
if (iconEl) iconEl.className = 'bi bi-cloud-upload';
|
||||
if (promptEl) promptEl.textContent = 'Tap to take / choose photo';
|
||||
}
|
||||
|
||||
// ── Signature pads ────────────────────────────────────────────────────────────
|
||||
@@ -800,6 +850,14 @@ document.getElementById('inspectionForm').addEventListener('submit', async funct
|
||||
responses[el.name.replace('field_', '')] = el.value;
|
||||
});
|
||||
|
||||
// Include server paths for image fields that were already AJAX-uploaded.
|
||||
// These are stored in no-name hidden inputs (id="field_<fid>_server_path")
|
||||
// so they don't interfere with the multipart form POST.
|
||||
form.querySelectorAll('input[id$="_server_path"]').forEach(function(pathEl) {
|
||||
const m = pathEl.id.match(/^field_(.+)_server_path$/);
|
||||
if (m && pathEl.value) responses[m[1]] = pathEl.value;
|
||||
});
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Saving…';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user