335 lines
16 KiB
HTML
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 %} |