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:
@@ -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'))
|
||||
Reference in New Issue
Block a user