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:
ske087
2026-07-08 22:56:21 +03:00
parent 95ea6a3a35
commit 55a088eff4
7 changed files with 581 additions and 3 deletions
@@ -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 %}