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:
@@ -26,7 +26,7 @@ def create_app(config_name='default'):
|
||||
login_manager.init_app(app)
|
||||
|
||||
# Import models so Flask-Migrate detects them
|
||||
from app.models import admin_user, user, asset, assignment, paperwork, audit_log, document_template # noqa: F401
|
||||
from app.models import admin_user, user, asset, assignment, paperwork, audit_log, document_template, maintenance # noqa: F401
|
||||
|
||||
# Register blueprints
|
||||
from app.routes.auth import bp as auth_bp
|
||||
@@ -39,6 +39,7 @@ def create_app(config_name='default'):
|
||||
from app.routes.settings import bp as settings_bp
|
||||
from app.routes.doc_templates import bp as doc_templates_bp
|
||||
from app.routes.internal import bp as internal_bp
|
||||
from app.routes.maintenance import bp as maintenance_bp
|
||||
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(dashboard_bp)
|
||||
@@ -50,6 +51,7 @@ def create_app(config_name='default'):
|
||||
app.register_blueprint(settings_bp)
|
||||
app.register_blueprint(doc_templates_bp)
|
||||
app.register_blueprint(internal_bp)
|
||||
app.register_blueprint(maintenance_bp)
|
||||
|
||||
# Inject common template variables
|
||||
from datetime import datetime, date
|
||||
|
||||
@@ -5,5 +5,6 @@ from app.models.assignment import Assignment
|
||||
from app.models.paperwork import Paperwork
|
||||
from app.models.audit_log import AuditLog
|
||||
from app.models.compliance_check import ComplianceCheck
|
||||
from app.models.maintenance import MaintenanceRecord
|
||||
|
||||
__all__ = ['AdminUser', 'User', 'Asset', 'Assignment', 'Paperwork', 'AuditLog', 'ComplianceCheck']
|
||||
__all__ = ['AdminUser', 'User', 'Asset', 'Assignment', 'Paperwork', 'AuditLog', 'ComplianceCheck', 'MaintenanceRecord']
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
from datetime import datetime
|
||||
from app.extensions import db
|
||||
|
||||
MAINTENANCE_STATUSES = [
|
||||
('open', 'Open / Diagnosed'),
|
||||
('in_progress', 'In Progress'),
|
||||
('closed', 'Closed'),
|
||||
]
|
||||
|
||||
|
||||
class MaintenanceRecord(db.Model):
|
||||
"""A maintenance or service event performed on a single asset."""
|
||||
__tablename__ = 'maintenance_records'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
asset_id = db.Column(db.Integer, db.ForeignKey('assets.id'), nullable=False)
|
||||
|
||||
start_date = db.Column(db.Date, nullable=False)
|
||||
end_date = db.Column(db.Date, nullable=True)
|
||||
|
||||
status = db.Column(db.String(20), nullable=False, default='open')
|
||||
|
||||
initial_diagnosis = db.Column(db.Text, nullable=True)
|
||||
action_taken = db.Column(db.Text, nullable=True)
|
||||
action_result = db.Column(db.Text, nullable=True)
|
||||
|
||||
created_by_id = db.Column(db.Integer, db.ForeignKey('admin_users.id'), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
asset = db.relationship('Asset', backref=db.backref('maintenance_records', lazy='dynamic'))
|
||||
created_by = db.relationship('AdminUser', foreign_keys=[created_by_id])
|
||||
|
||||
@property
|
||||
def is_open(self):
|
||||
return self.status != 'closed'
|
||||
|
||||
def __repr__(self):
|
||||
return f'<MaintenanceRecord asset={self.asset_id} status={self.status}>'
|
||||
@@ -7,8 +7,9 @@ from app.routes.paperwork import bp as paperwork_bp
|
||||
from app.routes.audit import bp as audit_bp
|
||||
from app.routes.settings import bp as settings_bp
|
||||
from app.routes.internal import bp as internal_bp
|
||||
from app.routes.maintenance import bp as maintenance_bp
|
||||
|
||||
__all__ = [
|
||||
'auth_bp', 'dashboard_bp', 'users_bp', 'assets_bp',
|
||||
'assignments_bp', 'paperwork_bp', 'audit_bp', 'settings_bp', 'internal_bp',
|
||||
'assignments_bp', 'paperwork_bp', 'audit_bp', 'settings_bp', 'internal_bp', 'maintenance_bp',
|
||||
]
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
from flask import (Blueprint, render_template, redirect, url_for,
|
||||
flash, request, current_app, jsonify)
|
||||
from flask_login import login_required, current_user
|
||||
from app.extensions import db
|
||||
from app.models.maintenance import MaintenanceRecord, MAINTENANCE_STATUSES
|
||||
from app.models.asset import Asset, ASSET_STATUSES
|
||||
from app.models.audit_log import AuditLog
|
||||
from app.utils.decorators import editor_required
|
||||
|
||||
bp = Blueprint('maintenance', __name__, url_prefix='/maintenance')
|
||||
|
||||
|
||||
def _parse_date(value):
|
||||
if not value:
|
||||
return None
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
try:
|
||||
return date.fromisoformat(value.strip())
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
|
||||
|
||||
def _log(action, record_id, description, old=None, new=None):
|
||||
entry = AuditLog(
|
||||
table_name='maintenance_records',
|
||||
record_id=record_id,
|
||||
action=action,
|
||||
old_values=json.dumps(old) if old else None,
|
||||
new_values=json.dumps(new) if new else None,
|
||||
performed_by_id=current_user.id,
|
||||
ip_address=request.remote_addr,
|
||||
description=description,
|
||||
)
|
||||
db.session.add(entry)
|
||||
|
||||
|
||||
# ── List ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@bp.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
status_filter = request.args.get('status', '')
|
||||
asset_filter = request.args.get('asset_id', type=int)
|
||||
page = request.args.get('page', 1, type=int)
|
||||
|
||||
query = MaintenanceRecord.query.join(MaintenanceRecord.asset)
|
||||
if status_filter:
|
||||
query = query.filter(MaintenanceRecord.status == status_filter)
|
||||
if asset_filter:
|
||||
query = query.filter(MaintenanceRecord.asset_id == asset_filter)
|
||||
|
||||
pagination = query.order_by(MaintenanceRecord.start_date.desc()).paginate(
|
||||
page=page, per_page=current_app.config['ITEMS_PER_PAGE'], error_out=False
|
||||
)
|
||||
|
||||
# Assets available for the "add record" dropdown
|
||||
all_assets = Asset.query.order_by(Asset.serial_number).all()
|
||||
|
||||
return render_template(
|
||||
'maintenance/index.html',
|
||||
pagination=pagination,
|
||||
status_filter=status_filter,
|
||||
asset_filter=asset_filter,
|
||||
statuses=MAINTENANCE_STATUSES,
|
||||
all_assets=all_assets,
|
||||
edit_record=None,
|
||||
)
|
||||
|
||||
|
||||
# ── Create ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@bp.route('/new', methods=['POST'])
|
||||
@login_required
|
||||
@editor_required
|
||||
def create():
|
||||
asset_id = request.form.get('asset_id', type=int)
|
||||
asset = Asset.query.get(asset_id) if asset_id else None
|
||||
if not asset:
|
||||
flash('Please select a valid asset.', 'danger')
|
||||
return redirect(url_for('maintenance.index'))
|
||||
|
||||
start = _parse_date(request.form.get('start_date'))
|
||||
if not start:
|
||||
flash('Start date is required.', 'danger')
|
||||
return redirect(url_for('maintenance.index'))
|
||||
|
||||
record = MaintenanceRecord(
|
||||
asset_id = asset.id,
|
||||
start_date = start,
|
||||
end_date = _parse_date(request.form.get('end_date')),
|
||||
status = request.form.get('status', 'open'),
|
||||
initial_diagnosis = request.form.get('initial_diagnosis', '').strip() or None,
|
||||
action_taken = request.form.get('action_taken', '').strip() or None,
|
||||
action_result = request.form.get('action_result', '').strip() or None,
|
||||
created_by_id = current_user.id,
|
||||
)
|
||||
db.session.add(record)
|
||||
|
||||
# Auto-set asset status to 'maintenance' when a new open record is created
|
||||
if record.status in ('open', 'in_progress') and asset.status != 'maintenance':
|
||||
asset.status = 'maintenance'
|
||||
|
||||
db.session.flush()
|
||||
_log('create', record.id,
|
||||
f'Created maintenance record for asset SN={asset.serial_number}',
|
||||
new={'asset_id': asset.id, 'status': record.status, 'start_date': str(start)})
|
||||
db.session.commit()
|
||||
flash(f'Maintenance record added for {asset.serial_number}.', 'success')
|
||||
return redirect(url_for('maintenance.index'))
|
||||
|
||||
|
||||
# ── Edit (GET — opens modal pre-filled) ─────────────────────────────────────
|
||||
|
||||
@bp.route('/<int:record_id>/edit')
|
||||
@login_required
|
||||
@editor_required
|
||||
def edit(record_id):
|
||||
record = MaintenanceRecord.query.get_or_404(record_id)
|
||||
all_assets = Asset.query.order_by(Asset.serial_number).all()
|
||||
status_filter = request.args.get('status', '')
|
||||
asset_filter = request.args.get('asset_id', type=int)
|
||||
page = request.args.get('page', 1, type=int)
|
||||
|
||||
query = MaintenanceRecord.query.join(MaintenanceRecord.asset)
|
||||
if status_filter:
|
||||
query = query.filter(MaintenanceRecord.status == status_filter)
|
||||
if asset_filter:
|
||||
query = query.filter(MaintenanceRecord.asset_id == asset_filter)
|
||||
|
||||
pagination = query.order_by(MaintenanceRecord.start_date.desc()).paginate(
|
||||
page=page, per_page=current_app.config['ITEMS_PER_PAGE'], error_out=False
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'maintenance/index.html',
|
||||
pagination=pagination,
|
||||
status_filter=status_filter,
|
||||
asset_filter=asset_filter,
|
||||
statuses=MAINTENANCE_STATUSES,
|
||||
all_assets=all_assets,
|
||||
edit_record=record,
|
||||
)
|
||||
|
||||
|
||||
# ── Update ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@bp.route('/<int:record_id>/update', methods=['POST'])
|
||||
@login_required
|
||||
@editor_required
|
||||
def update(record_id):
|
||||
record = MaintenanceRecord.query.get_or_404(record_id)
|
||||
old = {
|
||||
'status': record.status,
|
||||
'start_date': str(record.start_date),
|
||||
'end_date': str(record.end_date) if record.end_date else None,
|
||||
'initial_diagnosis': record.initial_diagnosis,
|
||||
'action_taken': record.action_taken,
|
||||
'action_result': record.action_result,
|
||||
}
|
||||
|
||||
new_status = request.form.get('status', record.status)
|
||||
record.start_date = _parse_date(request.form.get('start_date')) or record.start_date
|
||||
record.end_date = _parse_date(request.form.get('end_date'))
|
||||
record.status = new_status
|
||||
record.initial_diagnosis = request.form.get('initial_diagnosis', '').strip() or None
|
||||
record.action_taken = request.form.get('action_taken', '').strip() or None
|
||||
record.action_result = request.form.get('action_result', '').strip() or None
|
||||
|
||||
# If closed, clear asset's maintenance status (if no other open records)
|
||||
if new_status == 'closed' and record.asset:
|
||||
other_open = MaintenanceRecord.query.filter(
|
||||
MaintenanceRecord.asset_id == record.asset_id,
|
||||
MaintenanceRecord.status != 'closed',
|
||||
MaintenanceRecord.id != record.id,
|
||||
).first()
|
||||
if not other_open and record.asset.status == 'maintenance':
|
||||
record.asset.status = 'available'
|
||||
|
||||
_log('update', record.id,
|
||||
f'Updated maintenance record id={record_id}', old=old,
|
||||
new={'status': record.status, 'end_date': str(record.end_date) if record.end_date else None})
|
||||
db.session.commit()
|
||||
flash('Maintenance record updated.', 'success')
|
||||
return redirect(url_for('maintenance.index'))
|
||||
|
||||
|
||||
# ── Delete ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@bp.route('/<int:record_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
@editor_required
|
||||
def delete(record_id):
|
||||
record = MaintenanceRecord.query.get_or_404(record_id)
|
||||
asset_sn = record.asset.serial_number if record.asset else '?'
|
||||
_log('delete', record.id, f'Deleted maintenance record for asset SN={asset_sn}')
|
||||
db.session.delete(record)
|
||||
db.session.commit()
|
||||
flash('Maintenance record deleted.', 'success')
|
||||
return redirect(url_for('maintenance.index'))
|
||||
@@ -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