Phase 2: add Facility delete button
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
from flask_wtf.csrf import generate_csrf
|
||||||
from app import db
|
from app import db
|
||||||
from app.models.facility import Facility, Area
|
from app.models.facility import Facility, Area
|
||||||
from app.utils.forms import FacilityForm, AreaForm
|
from app.utils.forms import FacilityForm, AreaForm
|
||||||
from app.utils.decorators import supervisor_required
|
from app.utils.decorators import supervisor_required, admin_required
|
||||||
|
|
||||||
bp = Blueprint('facilities', __name__, url_prefix='/facilities')
|
bp = Blueprint('facilities', __name__, url_prefix='/facilities')
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ bp = Blueprint('facilities', __name__, url_prefix='/facilities')
|
|||||||
@login_required
|
@login_required
|
||||||
def list_facilities():
|
def list_facilities():
|
||||||
facilities = Facility.query.order_by(Facility.name).all()
|
facilities = Facility.query.order_by(Facility.name).all()
|
||||||
return render_template('facilities/list.html', facilities=facilities)
|
return render_template('facilities/list.html', facilities=facilities, csrf_token=generate_csrf())
|
||||||
|
|
||||||
@bp.route('/new', methods=['GET', 'POST'])
|
@bp.route('/new', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -41,7 +42,7 @@ def create_facility():
|
|||||||
def view_facility(facility_id):
|
def view_facility(facility_id):
|
||||||
facility = Facility.query.get_or_404(facility_id)
|
facility = Facility.query.get_or_404(facility_id)
|
||||||
areas = facility.areas.order_by(Area.name).all()
|
areas = facility.areas.order_by(Area.name).all()
|
||||||
return render_template('facilities/view.html', facility=facility, areas=areas)
|
return render_template('facilities/view.html', facility=facility, areas=areas, csrf_token=generate_csrf())
|
||||||
|
|
||||||
@bp.route('/<int:facility_id>/edit', methods=['GET', 'POST'])
|
@bp.route('/<int:facility_id>/edit', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -65,20 +66,19 @@ def edit_facility(facility_id):
|
|||||||
|
|
||||||
@bp.route('/<int:facility_id>/delete', methods=['POST'])
|
@bp.route('/<int:facility_id>/delete', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@supervisor_required
|
@admin_required
|
||||||
def delete_facility(facility_id):
|
def delete_facility(facility_id):
|
||||||
facility = Facility.query.get_or_404(facility_id)
|
facility = Facility.query.get_or_404(facility_id)
|
||||||
|
|
||||||
# Check if facility has inspections
|
|
||||||
if facility.inspections.count() > 0:
|
if facility.inspections.count() > 0:
|
||||||
flash('Cannot delete facility with existing inspections.', 'danger')
|
flash(f'Cannot delete "{facility.name}" — it has existing inspection records.', 'danger')
|
||||||
return redirect(url_for('facilities.view_facility', facility_id=facility.id))
|
return redirect(url_for('facilities.view_facility', facility_id=facility.id))
|
||||||
|
|
||||||
facility_name = facility.name
|
facility_name = facility.name
|
||||||
db.session.delete(facility)
|
db.session.delete(facility)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash(f'Facility "{facility_name}" deleted successfully.', 'success')
|
flash(f'Facility "{facility_name}" has been permanently deleted.', 'success')
|
||||||
return redirect(url_for('facilities.list_facilities'))
|
return redirect(url_for('facilities.list_facilities'))
|
||||||
|
|
||||||
# Area Management Routes
|
# Area Management Routes
|
||||||
@@ -114,7 +114,6 @@ def edit_area(area_id):
|
|||||||
area = Area.query.get_or_404(area_id)
|
area = Area.query.get_or_404(area_id)
|
||||||
form = AreaForm(obj=area)
|
form = AreaForm(obj=area)
|
||||||
|
|
||||||
# Populate facility choices
|
|
||||||
facilities = Facility.query.filter_by(active=True).order_by(Facility.name).all()
|
facilities = Facility.query.filter_by(active=True).order_by(Facility.name).all()
|
||||||
form.facility_id.choices = [(f.id, f.name) for f in facilities]
|
form.facility_id.choices = [(f.id, f.name) for f in facilities]
|
||||||
|
|
||||||
@@ -136,7 +135,6 @@ def delete_area(area_id):
|
|||||||
area = Area.query.get_or_404(area_id)
|
area = Area.query.get_or_404(area_id)
|
||||||
facility_id = area.facility_id
|
facility_id = area.facility_id
|
||||||
|
|
||||||
# Check if area has inspections
|
|
||||||
if area.inspections.count() > 0:
|
if area.inspections.count() > 0:
|
||||||
flash('Cannot delete area with existing inspections.', 'danger')
|
flash('Cannot delete area with existing inspections.', 'danger')
|
||||||
return redirect(url_for('facilities.view_facility', facility_id=facility_id))
|
return redirect(url_for('facilities.view_facility', facility_id=facility_id))
|
||||||
|
|||||||
@@ -29,23 +29,34 @@
|
|||||||
<span class="badge bg-secondary">Inactive</span>
|
<span class="badge bg-secondary">Inactive</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
{% if facility.address %}
|
{% if facility.address %}
|
||||||
<p class="card-text text-muted small">
|
<p class="card-text text-muted small">
|
||||||
<i class="bi bi-geo-alt"></i> {{ facility.address }}
|
<i class="bi bi-geo-alt"></i> {{ facility.address }}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<i class="bi bi-diagram-3"></i> {{ facility.areas.count() }} areas
|
<i class="bi bi-diagram-3"></i> {{ facility.areas.count() }} areas
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer bg-transparent">
|
<div class="card-footer bg-transparent d-flex gap-2">
|
||||||
<a href="{{ url_for('facilities.view_facility', facility_id=facility.id) }}" class="btn btn-sm btn-outline-primary">
|
<a href="{{ url_for('facilities.view_facility', facility_id=facility.id) }}" class="btn btn-sm btn-outline-primary">
|
||||||
<i class="bi bi-eye"></i> View Details
|
<i class="bi bi-eye"></i> View Details
|
||||||
</a>
|
</a>
|
||||||
|
{% if current_user.role == 'admin' %}
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-danger ms-auto"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#deleteModal"
|
||||||
|
data-facility-id="{{ facility.id }}"
|
||||||
|
data-facility-name="{{ facility.name }}"
|
||||||
|
data-inspection-count="{{ facility.inspections.count() }}">
|
||||||
|
<i class="bi bi-trash"></i> Delete
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,4 +68,76 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if current_user.role == 'admin' %}
|
||||||
|
<!-- 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>You are about to permanently delete:</p>
|
||||||
|
<p class="fw-bold fs-5" id="modalFacilityName"></p>
|
||||||
|
<div id="modalWarningBlock" class="alert alert-danger d-none">
|
||||||
|
<i class="bi bi-x-circle-fill"></i>
|
||||||
|
<strong>Cannot delete this facility.</strong> It has existing inspection records.
|
||||||
|
Please remove all associated inspections first.
|
||||||
|
</div>
|
||||||
|
<div id="modalConfirmBlock">
|
||||||
|
<p class="text-muted mb-0">This action is <strong>irreversible</strong>. All areas associated with this facility will also be deleted.</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="deleteFacilityForm" 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 == 'admin' %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const deleteModal = document.getElementById('deleteModal');
|
||||||
|
deleteModal.addEventListener('show.bs.modal', function (event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
const facilityId = button.getAttribute('data-facility-id');
|
||||||
|
const facilityName = button.getAttribute('data-facility-name');
|
||||||
|
const inspectionCount = parseInt(button.getAttribute('data-inspection-count'));
|
||||||
|
|
||||||
|
document.getElementById('modalFacilityName').textContent = facilityName;
|
||||||
|
document.getElementById('deleteFacilityForm').action = '/facilities/' + facilityId + '/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 %}
|
{% endblock %}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<h2><i class="bi bi-building"></i> {{ facility.name }}</h2>
|
<h2><i class="bi bi-building"></i> {{ facility.name }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-end">
|
<div class="col-md-4 text-end d-flex gap-2 justify-content-end align-items-start">
|
||||||
{% if current_user.role in ['admin', 'supervisor'] %}
|
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||||
<a href="{{ url_for('facilities.edit_facility', facility_id=facility.id) }}" class="btn btn-outline-primary">
|
<a href="{{ url_for('facilities.edit_facility', facility_id=facility.id) }}" class="btn btn-outline-primary">
|
||||||
<i class="bi bi-pencil"></i> Edit
|
<i class="bi bi-pencil"></i> Edit
|
||||||
@@ -16,6 +16,14 @@
|
|||||||
<i class="bi bi-plus-circle"></i> Add Area
|
<i class="bi bi-plus-circle"></i> Add Area
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if current_user.role == 'admin' %}
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-danger"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#deleteModal">
|
||||||
|
<i class="bi bi-trash-fill"></i> Delete Facility
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -51,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header bg-light">
|
||||||
@@ -103,6 +111,7 @@
|
|||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
</a>
|
</a>
|
||||||
<form method="POST" action="{{ url_for('facilities.delete_area', area_id=area.id) }}" class="d-inline" onsubmit="return confirm('Delete this area?');">
|
<form method="POST" action="{{ url_for('facilities.delete_area', area_id=area.id) }}" class="d-inline" onsubmit="return confirm('Delete this area?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -121,4 +130,51 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if current_user.role == 'admin' %}
|
||||||
|
<!-- 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>You are about to permanently delete:</p>
|
||||||
|
<p class="fw-bold fs-5">{{ facility.name }}</p>
|
||||||
|
{% if facility.inspections.count() > 0 %}
|
||||||
|
<div class="alert alert-danger mb-0">
|
||||||
|
<i class="bi bi-x-circle-fill"></i>
|
||||||
|
<strong>Cannot delete this facility.</strong> It has
|
||||||
|
<strong>{{ facility.inspections.count() }} inspection record(s)</strong> on file.
|
||||||
|
Please remove all associated inspections first.
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||||
|
This action is <strong>irreversible</strong>. All
|
||||||
|
<strong>{{ areas|length }} area(s)</strong> associated with this facility will also be deleted.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</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 method="POST" action="{{ url_for('facilities.delete_facility', facility_id=facility.id) }}" class="d-inline">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-danger"
|
||||||
|
{% if facility.inspections.count() > 0 %}disabled{% endif %}>
|
||||||
|
<i class="bi bi-trash-fill"></i> Delete Permanently
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user