Mar 03 2026: enhanced inspection and issue workflow 2
This commit is contained in:
@@ -344,13 +344,6 @@ def execute(inspection_id):
|
|||||||
flash('Inspection submitted successfully!', 'success')
|
flash('Inspection submitted successfully!', 'success')
|
||||||
return redirect(url_for('inspections.view', inspection_id=inspection_id))
|
return redirect(url_for('inspections.view', inspection_id=inspection_id))
|
||||||
|
|
||||||
elif action == 'flag':
|
|
||||||
# Save current form state as a draft so no work is lost,
|
|
||||||
# then send the inspector to the issue creation page.
|
|
||||||
_save_draft(inspection, responses)
|
|
||||||
db.session.commit()
|
|
||||||
return redirect(url_for('inspections.flag_issue', inspection_id=inspection_id))
|
|
||||||
|
|
||||||
else: # save draft
|
else: # save draft
|
||||||
_save_draft(inspection, responses)
|
_save_draft(inspection, responses)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@@ -381,6 +374,50 @@ def _save_draft(inspection, responses):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
# ── AJAX: save draft (used by Flag Issue button before navigating away) ───────
|
||||||
|
|
||||||
|
@bp.route('/<int:inspection_id>/save-draft', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def save_draft_ajax(inspection_id):
|
||||||
|
"""
|
||||||
|
Accepts a JSON body of { field_id: value } and persists it as a draft.
|
||||||
|
Returns JSON { ok: true } on success so the caller can navigate to flag_issue.
|
||||||
|
"""
|
||||||
|
inspection = Inspection.query.get_or_404(inspection_id)
|
||||||
|
|
||||||
|
if current_user.role == 'inspector' and inspection.inspector_id != current_user.id:
|
||||||
|
return jsonify({'ok': False, 'error': 'Access denied'}), 403
|
||||||
|
|
||||||
|
if inspection.status == 'completed':
|
||||||
|
return jsonify({'ok': False, 'error': 'Inspection already completed'}), 400
|
||||||
|
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
responses = data.get('responses', {})
|
||||||
|
|
||||||
|
# Merge with any previously saved responses so photo paths are preserved
|
||||||
|
existing_responses = {}
|
||||||
|
if inspection.notes:
|
||||||
|
try:
|
||||||
|
parsed = json.loads(inspection.notes)
|
||||||
|
if isinstance(parsed, dict) and '_form_data' in parsed:
|
||||||
|
existing_responses = parsed['_form_data']
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Only overwrite keys that are present in the submitted data;
|
||||||
|
# preserve photo paths for image fields not included in the JSON payload.
|
||||||
|
merged = {**existing_responses, **responses}
|
||||||
|
|
||||||
|
_save_responses(inspection, merged)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
current_app.logger.info(
|
||||||
|
'INSPECTION DRAFT SAVED (flag) | inspection_id=%s | by=%s',
|
||||||
|
inspection_id, current_user.username
|
||||||
|
)
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
|
||||||
|
|
||||||
# ── View ──────────────────────────────────────────────────────────────────────
|
# ── View ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@bp.route('/<int:inspection_id>')
|
@bp.route('/<int:inspection_id>')
|
||||||
|
|||||||
@@ -214,9 +214,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2 align-items-center">
|
<div class="d-flex gap-2 align-items-center">
|
||||||
<span class="freq-badge">{{ inspection.template.frequency|title }}</span>
|
<span class="freq-badge">{{ inspection.template.frequency|title }}</span>
|
||||||
<button type="submit" name="action" value="flag"
|
<button type="button"
|
||||||
class="btn btn-sm btn-outline-danger btn-outline-light"
|
class="btn btn-sm btn-outline-danger btn-outline-light"
|
||||||
onclick="return confirm('Your current progress will be saved automatically. Continue to flag an issue?')">
|
id="flagIssueBtn"
|
||||||
|
data-flag-url="{{ url_for('inspections.save_draft_ajax', inspection_id=inspection.id) }}"
|
||||||
|
data-flag-redirect="{{ url_for('inspections.flag_issue', inspection_id=inspection.id) }}">
|
||||||
<i class="bi bi-exclamation-triangle"></i> Flag Issue
|
<i class="bi bi-exclamation-triangle"></i> Flag Issue
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -674,5 +676,64 @@ document.getElementById('inspectionForm').addEventListener('submit', async funct
|
|||||||
alert('An error occurred. Please try again.');
|
alert('An error occurred. Please try again.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Flag Issue: save draft via AJAX then navigate ────────────────────────────
|
||||||
|
(function () {
|
||||||
|
const btn = document.getElementById('flagIssueBtn');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
btn.addEventListener('click', async function () {
|
||||||
|
const saveDraftUrl = btn.dataset.flagUrl;
|
||||||
|
const redirectUrl = btn.dataset.flagRedirect;
|
||||||
|
|
||||||
|
// Collect all current form field values (non-file inputs only)
|
||||||
|
const form = document.getElementById('inspectionForm');
|
||||||
|
const responses = {};
|
||||||
|
form.querySelectorAll('input, textarea, select').forEach(el => {
|
||||||
|
if (!el.name || !el.name.startsWith('field_')) return;
|
||||||
|
if (el.type === 'file') return; // files can't be JSON-serialised
|
||||||
|
if (el.type === 'checkbox') {
|
||||||
|
responses[el.name.replace('field_', '')] = el.checked ? 'true' : 'false';
|
||||||
|
} else if (el.type === 'radio') {
|
||||||
|
if (el.checked) responses[el.name.replace('field_', '')] = el.value;
|
||||||
|
} else {
|
||||||
|
responses[el.name.replace('field_', '')] = el.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Flush signature canvases to their hidden inputs before reading values
|
||||||
|
collectSignatures();
|
||||||
|
form.querySelectorAll('input[type="hidden"]').forEach(el => {
|
||||||
|
if (!el.name || !el.name.startsWith('field_')) return;
|
||||||
|
responses[el.name.replace('field_', '')] = el.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Saving…';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(saveDraftUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': document.querySelector('input[name="csrf_token"]').value,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ responses }),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.ok) {
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
} else {
|
||||||
|
alert('Could not save draft: ' + (json.error || 'Unknown error'));
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="bi bi-exclamation-triangle"></i> Flag Issue';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Flag Issue draft save error:', err);
|
||||||
|
// Fall back to navigating directly without saving
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}());
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user