Files
JQC_multi_tenant/app/templates/templates/list.html
T

335 lines
16 KiB
HTML

{% extends "base.html" %}
{% block title %}Inspection Templates{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-md-6">
<h2><i class="bi bi-file-earmark-text"></i> Inspection Templates</h2>
</div>
<div class="col-md-6 text-end">
{% if current_user.role in ['admin', 'director'] %}
<a href="{{ url_for('templates.create_template') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Create Template
</a>
{% endif %}
</div>
</div>
<div class="row">
{% for template in templates %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card shadow-sm h-100 {% if not template.active %}opacity-75 border-secondary{% endif %}">
<div class="card-body">
<h5 class="card-title d-flex align-items-start gap-2">
<a href="{{ url_for('templates.view_template', template_id=template.id) }}" class="text-decoration-none flex-grow-1">
{{ template.name }}
</a>
{% if template.active %}
<span class="badge bg-success flex-shrink-0">Active</span>
{% else %}
<span class="badge bg-secondary flex-shrink-0">Inactive</span>
{% endif %}
</h5>
<p class="card-text text-muted small">{{ template.description or 'No description' }}</p>
<div class="mt-3">
<span class="badge bg-info">{{ template.frequency|title }}</span>
<small class="text-muted ms-2">
<i class="bi bi-check2-square"></i> {{ template.checklist_items.count() }} items
</small>
</div>
{# phase52 — who may use this form. No links = shared with all. #}
<div class="mt-2">
{% if template.is_shared %}
<span class="badge bg-light text-dark border"
title="Available on every contract">
<i class="bi bi-globe2"></i> Shared
</span>
{% else %}
{% for link in template.contract_links %}
<span class="badge bg-primary"
title="Only available on this contract">
<i class="bi bi-briefcase"></i>
{{ link.project.name if link.project else 'contract #' ~ link.project_id }}
</span>
{% endfor %}
{% endif %}
</div>
</div>
<div class="card-footer bg-transparent d-flex gap-2 align-items-center flex-wrap">
<a href="{{ url_for('templates.view_template', template_id=template.id) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> View
</a>
{% if current_user.role in ['admin', 'director'] %}
<!-- Rename -->
<button type="button"
class="btn btn-sm btn-outline-secondary"
data-bs-toggle="modal"
data-bs-target="#renameModal"
data-template-id="{{ template.id }}"
data-template-name="{{ template.name }}"
data-template-description="{{ template.description or '' }}"
data-template-frequency="{{ template.frequency or 'daily' }}"
data-template-contracts="{{ template.contract_ids|join(',') }}"
title="Edit template details">
<i class="bi bi-pencil"></i> Edit
</button>
<!-- Duplicate -->
<form method="POST"
action="{{ url_for('templates.duplicate_template', template_id=template.id) }}"
class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-outline-secondary"
title="Duplicate template">
<i class="bi bi-copy"></i> Duplicate
</button>
</form>
<!-- Toggle active/inactive -->
<form method="POST"
action="{{ url_for('templates.toggle_active', template_id=template.id) }}"
class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if template.active %}
<button type="submit" class="btn btn-sm btn-outline-warning"
title="Deactivate template">
<i class="bi bi-pause-circle"></i> Deactivate
</button>
{% else %}
<button type="submit" class="btn btn-sm btn-outline-success"
title="Activate template">
<i class="bi bi-play-circle"></i> Activate
</button>
{% endif %}
</form>
<!-- Delete -->
<button type="button"
class="btn btn-sm btn-outline-danger ms-auto"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
data-template-id="{{ template.id }}"
data-template-name="{{ template.name }}"
data-inspection-count="{{ template.inspections.count() }}"
title="Delete template">
<i class="bi bi-trash"></i> Delete
</button>
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> No templates created yet.
</div>
</div>
{% endfor %}
</div>
{% if current_user.role in ['admin', 'director'] %}
<!-- Edit Template Modal -->
<div class="modal fade" id="renameModal" tabindex="-1" aria-labelledby="renameModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="renameModalLabel">
<i class="bi bi-pencil"></i> Edit Template
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form id="renameTemplateForm" method="POST" action="">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="renameInput" class="form-label fw-semibold">Name <span class="text-danger">*</span></label>
<input type="text" id="renameInput" name="name"
class="form-control" maxlength="255"
placeholder="Enter template name" required>
</div>
<div class="mb-3">
<label for="editDescription" class="form-label fw-semibold">Description</label>
<textarea id="editDescription" name="description"
class="form-control" rows="3"
placeholder="Optional description"></textarea>
</div>
<div class="mb-3">
<label for="editFrequency" class="form-label fw-semibold">Frequency</label>
<select id="editFrequency" name="frequency" class="form-select">
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
<option value="quarterly">Quarterly</option>
</select>
</div>
{# phase52 — which contracts may use this form. The hidden
marker tells the route this modal really did include the
field, so an empty selection means "share it" rather
than "no field was posted, leave it alone". #}
<div class="mb-1">
<label for="editContracts" class="form-label fw-semibold">
Available on contracts
</label>
<input type="hidden" name="contracts_present" value="1">
<select id="editContracts" name="contract_ids"
class="form-select" multiple size="6">
{% for p in contracts %}
<option value="{{ p.id }}">{{ p.name }}</option>
{% endfor %}
</select>
<div class="form-text">
Leave <strong>nothing selected</strong> to share this form with
every contract (this is how all existing forms are set).
Select one or more contracts to restrict it to them — it is
then hidden from every other customer, on the web and in the
iPad app. Ctrl/Cmd-click to select several.
</div>
<button type="button" id="clearContracts"
class="btn btn-sm btn-link px-0 mt-1">
Clear selection (make shared)
</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle"></i> Cancel
</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteModalLabel">
<i class="bi bi-exclamation-triangle-fill"></i> Confirm Deletion
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="mb-1">You are about to permanently delete:</p>
<p class="fw-bold fs-5 mb-3" id="modalTemplateName"></p>
<!-- Shown when template has inspections -->
<div id="modalWarningBlock" class="alert alert-danger d-none mb-0">
<i class="bi bi-x-circle-fill"></i>
<strong>Cannot delete this template.</strong>
It has existing inspection records linked to it.
Remove all associated inspections first.
</div>
<!-- Shown when safe to delete -->
<div id="modalConfirmBlock">
<p class="text-muted mb-0">
This action is <strong>irreversible</strong>.
All checklist items and form schema for this template will be permanently removed.
</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle"></i> Cancel
</button>
<form id="deleteTemplateForm" method="POST" action="" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" id="confirmDeleteBtn" class="btn btn-danger">
<i class="bi bi-trash-fill"></i> Delete Permanently
</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
{% if current_user.role in ['admin', 'director'] %}
<script>
document.addEventListener('DOMContentLoaded', function () {
// Edit template modal
const renameModal = document.getElementById('renameModal');
renameModal.addEventListener('show.bs.modal', function (event) {
const btn = event.relatedTarget;
const templateId = btn.getAttribute('data-template-id');
const templateName = btn.getAttribute('data-template-name');
const templateDesc = btn.getAttribute('data-template-description');
const templateFreq = btn.getAttribute('data-template-frequency');
document.getElementById('renameInput').value = templateName;
document.getElementById('editDescription').value = templateDesc;
document.getElementById('editFrequency').value = templateFreq;
// Pre-tick the contracts this form is currently restricted to. An
// empty attribute means it is shared, so nothing is selected.
const contractSel = document.getElementById('editContracts');
if (contractSel) {
const current = (btn.getAttribute('data-template-contracts') || '')
.split(',').filter(Boolean);
Array.from(contractSel.options).forEach(function (o) {
o.selected = current.indexOf(o.value) !== -1;
});
}
document.getElementById('renameTemplateForm').action =
'/templates/' + templateId + '/rename';
renameModal.addEventListener('shown.bs.modal', function focusInput() {
const input = document.getElementById('renameInput');
input.select();
renameModal.removeEventListener('shown.bs.modal', focusInput);
});
});
const clearBtn = document.getElementById('clearContracts');
if (clearBtn) {
clearBtn.addEventListener('click', function () {
const sel = document.getElementById('editContracts');
Array.from(sel.options).forEach(function (o) { o.selected = false; });
});
}
const deleteModal = document.getElementById('deleteModal');
deleteModal.addEventListener('show.bs.modal', function (event) {
const btn = event.relatedTarget;
const templateId = btn.getAttribute('data-template-id');
const templateName = btn.getAttribute('data-template-name');
const inspectionCount = parseInt(btn.getAttribute('data-inspection-count'), 10);
document.getElementById('modalTemplateName').textContent = templateName;
document.getElementById('deleteTemplateForm').action =
'/templates/' + templateId + '/delete';
const warningBlock = document.getElementById('modalWarningBlock');
const confirmBlock = document.getElementById('modalConfirmBlock');
const confirmBtn = document.getElementById('confirmDeleteBtn');
if (inspectionCount > 0) {
warningBlock.classList.remove('d-none');
confirmBlock.classList.add('d-none');
confirmBtn.disabled = true;
} else {
warningBlock.classList.add('d-none');
confirmBlock.classList.remove('d-none');
confirmBtn.disabled = false;
}
});
});
</script>
{% endif %}
{% endblock %}