Aug 17 - Update bulk actions for inspection/issue list
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
{# ── Bulk-action toolbar for the inspections list ─────────────────────────────
|
||||
Included by BOTH inspections/list.html and modern/inspections/list.html —
|
||||
edit here, not in either copy.
|
||||
|
||||
Same structure as the issues toolbar: the form sits OUTSIDE the table and
|
||||
row checkboxes join it via the HTML5 `form` attribute, so the per-row
|
||||
delete form inside the table is never nested (rule 9).
|
||||
|
||||
Export is offered to anyone who can see the list — it is read-only and the
|
||||
route re-applies the viewer's facility scope to the submitted ids. The three
|
||||
mutating actions are admin/director only.
|
||||
#}
|
||||
{% set can_manage = current_user.role in ['admin', 'director'] %}
|
||||
<form method="POST" id="inspectionsBulkForm"
|
||||
action="{{ url_for('inspections.bulk_action') }}"
|
||||
class="border-bottom bg-light px-3 py-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="next" value="{{ current_url() }}">
|
||||
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<span class="small fw-semibold text-nowrap">
|
||||
<span class="bulk-count">0</span> selected
|
||||
</span>
|
||||
<span class="text-muted small d-none d-md-inline">|</span>
|
||||
|
||||
<button type="submit" name="action" value="export"
|
||||
class="btn btn-sm btn-outline-secondary text-nowrap" data-bulk-action>
|
||||
<i class="bi bi-file-earmark-pdf"></i> Export Selected
|
||||
</button>
|
||||
|
||||
{% if can_manage %}
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<input type="text" name="follow_up_note" class="form-control form-control-sm"
|
||||
style="min-width:180px;font-size:.8rem;"
|
||||
placeholder="Follow-up note (optional)"
|
||||
aria-label="Follow-up note applied to all selected">
|
||||
<button type="submit" name="action" value="flag_followup"
|
||||
class="btn btn-sm btn-outline-warning text-nowrap" data-bulk-action
|
||||
data-bulk-confirm="Request a follow-up on the selected inspections? Ones not yet submitted, or already flagged, are skipped.">
|
||||
<i class="bi bi-flag"></i> Request Follow-up
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="submit" name="action" value="clear_followup"
|
||||
class="btn btn-sm btn-outline-success text-nowrap" data-bulk-action
|
||||
data-bulk-confirm="Clear the follow-up flag on the selected inspections?">
|
||||
<i class="bi bi-flag-fill"></i> Clear Follow-up
|
||||
</button>
|
||||
|
||||
<button type="submit" name="action" value="delete"
|
||||
class="btn btn-sm btn-outline-danger text-nowrap ms-auto" data-bulk-action
|
||||
data-bulk-confirm="Permanently delete the selected inspections and their photos? This cannot be undone.">
|
||||
<i class="bi bi-trash"></i> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,79 @@
|
||||
{# ── Bulk-action toolbar for the issues list ──────────────────────────────────
|
||||
Included by BOTH issues/list.html and modern/issues/list.html — edit here,
|
||||
not in either copy.
|
||||
|
||||
The form lives OUTSIDE the table on purpose. Row checkboxes join it with the
|
||||
HTML5 `form="issuesBulkForm"` attribute instead of being wrapped by it, so
|
||||
the per-row delete / unfollow forms inside the table are never nested inside
|
||||
this one (rule 9 — browsers silently discard nested forms, and the row
|
||||
actions would stop working with no error).
|
||||
|
||||
`next` carries the current filtered list URL so the action returns here
|
||||
rather than to the bare index.
|
||||
|
||||
Requires from the view: `staff` (assignable users).
|
||||
#}
|
||||
{% set can_manage = current_user.role in ['admin', 'director', 'auditor'] %}
|
||||
{% set can_delete = current_user.role in ['admin', 'director'] %}
|
||||
{% if can_manage or can_delete %}
|
||||
<form method="POST" id="issuesBulkForm"
|
||||
action="{{ url_for('issues.bulk_action') }}"
|
||||
class="border-bottom bg-light px-3 py-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="next" value="{{ current_url() }}">
|
||||
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<span class="small fw-semibold text-nowrap">
|
||||
<span class="bulk-count">0</span> selected
|
||||
</span>
|
||||
<span class="text-muted small d-none d-md-inline">|</span>
|
||||
|
||||
{% if can_manage %}
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<select name="assigned_to" class="form-select form-select-sm"
|
||||
style="min-width:150px;font-size:.8rem;" aria-label="Assign selected to">
|
||||
<option value="0">— Unassigned —</option>
|
||||
{% for u in staff %}
|
||||
<option value="{{ u.id }}">{{ u.display_name }}{{ ' (Customer)' if u.is_external_inspector }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" name="action" value="assign"
|
||||
class="btn btn-sm btn-outline-primary text-nowrap" data-bulk-action
|
||||
data-bulk-confirm="Assign the selected issues to the chosen user?">
|
||||
<i class="bi bi-person-check"></i> Assign
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<select name="status" class="form-select form-select-sm"
|
||||
style="min-width:150px;font-size:.8rem;" aria-label="Set status of selected">
|
||||
<option value="">— Set status… —</option>
|
||||
<option value="open">Open</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
<option value="pending_verification">Pending Verification</option>
|
||||
<option value="resolved">Resolved</option>
|
||||
</select>
|
||||
<button type="submit" name="action" value="status"
|
||||
class="btn btn-sm btn-outline-primary text-nowrap" data-bulk-action
|
||||
data-bulk-confirm="Change the status of the selected issues?">
|
||||
<i class="bi bi-arrow-repeat"></i> Apply
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="submit" name="action" value="verify"
|
||||
class="btn btn-sm btn-outline-success text-nowrap" data-bulk-action
|
||||
data-bulk-confirm="Verify and close the selected issues? Issues that are not awaiting verification are skipped.">
|
||||
<i class="bi bi-patch-check"></i> Verify & Close
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if can_delete %}
|
||||
<button type="submit" name="action" value="delete"
|
||||
class="btn btn-sm btn-outline-danger text-nowrap ms-auto" data-bulk-action
|
||||
data-bulk-confirm="Permanently delete the selected issues and their photos? This cannot be undone.">
|
||||
<i class="bi bi-trash"></i> Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,77 @@
|
||||
{# ── Shared row-selection behaviour for bulk-action list pages ────────────────
|
||||
Included by the issues and inspections list templates (classic + modern).
|
||||
Generic on purpose — it keys off classes/attributes, not page-specific ids,
|
||||
so both pages share one implementation:
|
||||
|
||||
.bulk-check one per row (name=issue_ids / inspection_ids)
|
||||
.bulk-check-all the header select-all box
|
||||
.bulk-count element whose text becomes the selected count
|
||||
[data-bulk-action] submit buttons, disabled while nothing is selected
|
||||
[data-bulk-confirm] optional confirm text, count substituted for {n}
|
||||
|
||||
Guarding the submit on a zero selection matters: the browser would happily
|
||||
POST an empty id list, and the route would flash "No issues selected" after
|
||||
a full page round trip.
|
||||
#}
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var boxes = Array.prototype.slice.call(document.querySelectorAll('.bulk-check'));
|
||||
var all = document.querySelector('.bulk-check-all');
|
||||
var counts = Array.prototype.slice.call(document.querySelectorAll('.bulk-count'));
|
||||
var btns = Array.prototype.slice.call(document.querySelectorAll('[data-bulk-action]'));
|
||||
if (!boxes.length) return;
|
||||
|
||||
function selected() {
|
||||
return boxes.filter(function (b) { return b.checked; });
|
||||
}
|
||||
|
||||
function sync() {
|
||||
var n = selected().length;
|
||||
counts.forEach(function (el) { el.textContent = n; });
|
||||
btns.forEach(function (b) { b.disabled = (n === 0); });
|
||||
if (all) {
|
||||
all.checked = (n > 0 && n === boxes.length);
|
||||
// Distinguishes "some" from "none"/"all" in the header box.
|
||||
all.indeterminate = (n > 0 && n < boxes.length);
|
||||
}
|
||||
}
|
||||
|
||||
boxes.forEach(function (b) { b.addEventListener('change', sync); });
|
||||
|
||||
if (all) {
|
||||
all.addEventListener('change', function () {
|
||||
boxes.forEach(function (b) { b.checked = all.checked; });
|
||||
sync();
|
||||
});
|
||||
}
|
||||
|
||||
// Shift-click selects the range from the last clicked box — the usual
|
||||
// convention, and the difference between ticking 3 boxes and 40.
|
||||
var lastIndex = null;
|
||||
boxes.forEach(function (b, i) {
|
||||
b.addEventListener('click', function (e) {
|
||||
if (e.shiftKey && lastIndex !== null) {
|
||||
var lo = Math.min(lastIndex, i), hi = Math.max(lastIndex, i);
|
||||
for (var j = lo; j <= hi; j++) { boxes[j].checked = b.checked; }
|
||||
sync();
|
||||
}
|
||||
lastIndex = i;
|
||||
});
|
||||
});
|
||||
|
||||
btns.forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
var n = selected().length;
|
||||
if (n === 0) { e.preventDefault(); return; }
|
||||
var msg = btn.getAttribute('data-bulk-confirm');
|
||||
if (msg && !window.confirm(msg.replace('{n}', n) + '\n\n' + n + ' selected.')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
sync();
|
||||
}());
|
||||
</script>
|
||||
Reference in New Issue
Block a user