IT Assets: add Maintenance page with modal form
New database table: maintenance_records - Fields: asset, start_date, end_date, status (open/in_progress/closed), initial_diagnosis, action_taken, action_result New routes (/maintenance/): - List with status + asset filters and pagination - POST /new — create record (editor+); auto-sets asset status to 'maintenance' - GET /<id>/edit — opens list page with modal pre-filled - POST /<id>/update — save edits; auto-clears 'maintenance' status on close - POST /<id>/delete — remove record UI: - Bootstrap modal form shared for add and edit - Clicking any table row opens the modal pre-filled with that row's data - Asset field locked in edit mode (cannot change asset) - Delete button per row (with confirmation) - Sidebar: 'Maintenance' link added under Hardware section
This commit is contained in:
@@ -139,6 +139,10 @@
|
||||
class="nav-link {% if request.blueprint == 'assets' %}active{% endif %}">
|
||||
<i class="bi bi-laptop"></i> Assets
|
||||
</a>
|
||||
<a href="{{ url_for('maintenance.index') }}"
|
||||
class="nav-link {% if request.blueprint == 'maintenance' %}active{% endif %}">
|
||||
<i class="bi bi-wrench-adjustable"></i> Maintenance
|
||||
</a>
|
||||
{% if current_user.is_editor %}
|
||||
<a href="{{ url_for('assets.create') }}"
|
||||
class="nav-link {% if request.endpoint == 'assets.create' %}active{% endif %}">
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Maintenance – IT Asset Management{% endblock %}
|
||||
{% block breadcrumb %}
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
|
||||
<li class="breadcrumb-item active">Maintenance</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.status-badge-open { background: #ffc107; color: #000; }
|
||||
.status-badge-in_progress { background: #0d6efd; color: #fff; }
|
||||
.status-badge-closed { background: #198754; color: #fff; }
|
||||
tr.clickable-row { cursor: pointer; }
|
||||
tr.clickable-row:hover td { background: #f0f5ff; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header d-flex align-items-center justify-content-between mb-4">
|
||||
<h1><i class="bi bi-wrench-adjustable me-2"></i>Maintenance</h1>
|
||||
{% if current_user.is_editor %}
|
||||
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#maintenanceModal"
|
||||
id="btnAddRecord">
|
||||
<i class="bi bi-plus-circle me-1"></i>Add Record
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<form method="GET" class="row g-2 mb-3">
|
||||
<div class="col-auto">
|
||||
<select name="status" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
<option value="">All statuses</option>
|
||||
{% for val, label in statuses %}
|
||||
<option value="{{ val }}" {% if status_filter == val %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<select name="asset_id" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
<option value="">All assets</option>
|
||||
{% for a in all_assets %}
|
||||
<option value="{{ a.id }}" {% if asset_filter == a.id %}selected{% endif %}>
|
||||
{{ a.serial_number }}{% if a.brand %} – {{ a.brand }}{% endif %}{% if a.model %} {{ a.model }}{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="{{ url_for('maintenance.index') }}" class="btn btn-sm btn-outline-secondary">Clear</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Asset</th>
|
||||
<th>Start Date</th>
|
||||
<th>End Date</th>
|
||||
<th>Status</th>
|
||||
<th>Initial Diagnosis</th>
|
||||
<th>Action Taken</th>
|
||||
<th>Result</th>
|
||||
{% if current_user.is_editor %}<th></th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in pagination.items %}
|
||||
<tr class="{% if current_user.is_editor %}clickable-row{% endif %}"
|
||||
{% if current_user.is_editor %}
|
||||
data-bs-toggle="modal" data-bs-target="#maintenanceModal"
|
||||
data-id="{{ r.id }}"
|
||||
data-asset="{{ r.asset_id }}"
|
||||
data-start="{{ r.start_date.isoformat() if r.start_date else '' }}"
|
||||
data-end="{{ r.end_date.isoformat() if r.end_date else '' }}"
|
||||
data-status="{{ r.status }}"
|
||||
data-diagnosis="{{ r.initial_diagnosis or '' }}"
|
||||
data-action="{{ r.action_taken or '' }}"
|
||||
data-result="{{ r.action_result or '' }}"
|
||||
{% endif %}>
|
||||
<td>
|
||||
<a href="{{ url_for('assets.detail', asset_id=r.asset.id) }}" onclick="event.stopPropagation()">
|
||||
<strong>{{ r.asset.serial_number }}</strong>
|
||||
</a>
|
||||
{% if r.asset.brand %}<br><small class="text-muted">{{ r.asset.brand }} {{ r.asset.model or '' }}</small>{% endif %}
|
||||
</td>
|
||||
<td>{{ r.start_date.strftime('%d/%m/%Y') if r.start_date else '—' }}</td>
|
||||
<td>{{ r.end_date.strftime('%d/%m/%Y') if r.end_date else '—' }}</td>
|
||||
<td>
|
||||
<span class="badge status-badge-{{ r.status }}">
|
||||
{% for val, label in statuses %}{% if val == r.status %}{{ label }}{% endif %}{% endfor %}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span title="{{ r.initial_diagnosis or '' }}">
|
||||
{{ (r.initial_diagnosis or '—')[:60] }}{% if r.initial_diagnosis and r.initial_diagnosis|length > 60 %}…{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span title="{{ r.action_taken or '' }}">
|
||||
{{ (r.action_taken or '—')[:60] }}{% if r.action_taken and r.action_taken|length > 60 %}…{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span title="{{ r.action_result or '' }}">
|
||||
{{ (r.action_result or '—')[:60] }}{% if r.action_result and r.action_result|length > 60 %}…{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
{% if current_user.is_editor %}
|
||||
<td onclick="event.stopPropagation()">
|
||||
<form method="POST" action="{{ url_for('maintenance.delete', record_id=r.id) }}"
|
||||
onsubmit="return confirm('Delete this maintenance record?')">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger py-0 px-2">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="{{ 8 if current_user.is_editor else 7 }}" class="text-center text-muted py-4">
|
||||
No maintenance records found.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if pagination.pages > 1 %}
|
||||
<div class="card-footer bg-white d-flex justify-content-between align-items-center py-2">
|
||||
<small class="text-muted">
|
||||
Showing {{ pagination.first }}–{{ pagination.last }} of {{ pagination.total }}
|
||||
</small>
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
{% if pagination.has_prev %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('maintenance.index', page=pagination.prev_num, status=status_filter, asset_id=asset_filter) }}">‹</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for p in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||||
{% if p %}
|
||||
<li class="page-item {% if p == pagination.page %}active{% endif %}">
|
||||
<a class="page-link" href="{{ url_for('maintenance.index', page=p, status=status_filter, asset_id=asset_filter) }}">{{ p }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if pagination.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('maintenance.index', page=pagination.next_num, status=status_filter, asset_id=asset_filter) }}">›</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- ═══ Maintenance Record Modal (Add / Edit) ═══════════════════════════════ -->
|
||||
{% if current_user.is_editor %}
|
||||
<div class="modal fade" id="maintenanceModal" tabindex="-1" aria-labelledby="maintenanceModalLabel">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="maintenanceModalLabel">
|
||||
<i class="bi bi-wrench-adjustable me-2"></i>
|
||||
<span id="modalTitleText">Add Maintenance Record</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<form id="maintenanceForm" method="POST" action="{{ url_for('maintenance.create') }}">
|
||||
<div class="modal-body">
|
||||
|
||||
<!-- Asset -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Asset <span class="text-danger">*</span></label>
|
||||
<select name="asset_id" id="fieldAsset" class="form-select" required>
|
||||
<option value="">— select asset —</option>
|
||||
{% for a in all_assets %}
|
||||
<option value="{{ a.id }}">
|
||||
{{ a.serial_number }}{% if a.brand %} – {{ a.brand }}{% endif %}{% if a.model %} {{ a.model }}{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<!-- Start date -->
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Start Date <span class="text-danger">*</span></label>
|
||||
<input type="date" name="start_date" id="fieldStart" class="form-control" required>
|
||||
</div>
|
||||
<!-- End date -->
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">End Date</label>
|
||||
<input type="date" name="end_date" id="fieldEnd" class="form-control">
|
||||
</div>
|
||||
<!-- Status -->
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Status</label>
|
||||
<select name="status" id="fieldStatus" class="form-select">
|
||||
{% for val, label in statuses %}
|
||||
<option value="{{ val }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Initial diagnosis -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Initial Diagnosis</label>
|
||||
<textarea name="initial_diagnosis" id="fieldDiagnosis" class="form-control" rows="3"
|
||||
placeholder="Describe the fault or reason for maintenance…"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Action taken -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Action Taken</label>
|
||||
<textarea name="action_taken" id="fieldAction" class="form-control" rows="3"
|
||||
placeholder="Describe the work performed…"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Finished action result -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Finished Action Result</label>
|
||||
<textarea name="action_result" id="fieldResult" class="form-control" rows="3"
|
||||
placeholder="Outcome, parts replaced, verified status…"></textarea>
|
||||
</div>
|
||||
|
||||
</div><!-- /.modal-body -->
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-floppy me-1"></i><span id="submitBtnText">Save Record</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% if current_user.is_editor %}
|
||||
<script>
|
||||
const modal = document.getElementById('maintenanceModal');
|
||||
const form = document.getElementById('maintenanceForm');
|
||||
const titleText = document.getElementById('modalTitleText');
|
||||
const submitText = document.getElementById('submitBtnText');
|
||||
|
||||
const createUrl = "{{ url_for('maintenance.create') }}";
|
||||
const updateUrl = (id) => `/maintenance/${id}/update`;
|
||||
|
||||
// ── Open for EDIT when a table row is clicked ────────────────────────────────
|
||||
document.querySelectorAll('tr[data-id]').forEach(row => {
|
||||
row.addEventListener('click', () => {
|
||||
const id = row.dataset.id;
|
||||
|
||||
titleText.textContent = 'Edit Maintenance Record';
|
||||
submitText.textContent = 'Update Record';
|
||||
form.action = updateUrl(id);
|
||||
|
||||
document.getElementById('fieldAsset').value = row.dataset.asset;
|
||||
document.getElementById('fieldStart').value = row.dataset.start;
|
||||
document.getElementById('fieldEnd').value = row.dataset.end;
|
||||
document.getElementById('fieldStatus').value = row.dataset.status;
|
||||
document.getElementById('fieldDiagnosis').value= row.dataset.diagnosis;
|
||||
document.getElementById('fieldAction').value = row.dataset.action;
|
||||
document.getElementById('fieldResult').value = row.dataset.result;
|
||||
|
||||
// Lock asset field in edit mode (asset cannot change)
|
||||
document.getElementById('fieldAsset').disabled = true;
|
||||
});
|
||||
});
|
||||
|
||||
// ── Reset to ADD mode when modal is opened via the + button ─────────────────
|
||||
document.getElementById('btnAddRecord').addEventListener('click', () => {
|
||||
titleText.textContent = 'Add Maintenance Record';
|
||||
submitText.textContent = 'Save Record';
|
||||
form.action = createUrl;
|
||||
form.reset();
|
||||
document.getElementById('fieldAsset').disabled = false;
|
||||
// Default start date to today
|
||||
document.getElementById('fieldStart').value = new Date().toISOString().split('T')[0];
|
||||
});
|
||||
|
||||
// ── Re-enable asset select before submit (disabled fields are not posted) ────
|
||||
form.addEventListener('submit', () => {
|
||||
document.getElementById('fieldAsset').disabled = false;
|
||||
});
|
||||
|
||||
// ── If page loaded with edit_record (server-side edit link), auto-open modal ─
|
||||
{% if edit_record %}
|
||||
const editRow = document.querySelector('tr[data-id="{{ edit_record.id }}"]');
|
||||
if (editRow) editRow.click();
|
||||
else {
|
||||
// Fallback: populate manually
|
||||
titleText.textContent = 'Edit Maintenance Record';
|
||||
submitText.textContent = 'Update Record';
|
||||
form.action = updateUrl({{ edit_record.id }});
|
||||
document.getElementById('fieldAsset').value = '{{ edit_record.asset_id }}';
|
||||
document.getElementById('fieldStart').value = '{{ edit_record.start_date.isoformat() if edit_record.start_date else "" }}';
|
||||
document.getElementById('fieldEnd').value = '{{ edit_record.end_date.isoformat() if edit_record.end_date else "" }}';
|
||||
document.getElementById('fieldStatus').value = '{{ edit_record.status }}';
|
||||
document.getElementById('fieldDiagnosis').value= {{ edit_record.initial_diagnosis | tojson }};
|
||||
document.getElementById('fieldAction').value = {{ edit_record.action_taken | tojson }};
|
||||
document.getElementById('fieldResult').value = {{ edit_record.action_result | tojson }};
|
||||
document.getElementById('fieldAsset').disabled = true;
|
||||
new bootstrap.Modal(document.getElementById('maintenanceModal')).show();
|
||||
}
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user