diff --git a/app/routes/inspections.py b/app/routes/inspections.py index cbc6b22..7c3fc43 100644 --- a/app/routes/inspections.py +++ b/app/routes/inspections.py @@ -16,7 +16,7 @@ from app.models.issue import Issue from app.models.user import User from app.utils.forms import StartInspectionForm, IssueForm from app.utils.decorators import supervisor_required -from app.utils.pdf_export import generate_inspection_pdf +from app.utils.pdf_export import generate_inspection_pdf, generate_inspections_list_pdf from app.utils.notifications import notify, notify_customers_for_facility, notify_by_matrix from app.models.notification import ( EVENT_INSPECTION_DONE, EVENT_ISSUE_ASSIGNED, @@ -978,6 +978,127 @@ def flag_issue(inspection_id): form=form, inspection=inspection) +# ── Export filtered list to PDF ─────────────────────────────────────────────── + +@bp.route('/export-list-pdf') +@login_required +def export_list_pdf(): + """Generate and stream a PDF of the currently filtered inspection list.""" + q = Inspection.query.options( + joinedload(Inspection.facility), + joinedload(Inspection.template), + joinedload(Inspection.inspector), + joinedload(Inspection.area), + ).order_by(Inspection.inspection_date.desc()) + + if current_user.role == 'inspector': + fids = get_inspector_scope(current_user) + if not fids: + q = q.filter(False) + else: + q = q.filter(Inspection.facility_id.in_(fids)) + elif current_user.role == 'customer': + customer_facility_ids = get_customer_scope(current_user) + if not customer_facility_ids: + q = q.filter(False) + else: + q = q.filter(Inspection.facility_id.in_(customer_facility_ids)) + + status_filter = request.args.get('status', '') + facility_filter = request.args.get('facility_id', '') + contract_filter = request.args.get('contract_id', '') + date_from_filter = request.args.get('date_from', '') + date_to_filter = request.args.get('date_to', '') + score_min_filter = request.args.get('score_min', '') + score_max_filter = request.args.get('score_max', '') + inspector_filter = request.args.get('inspector_id', '') + + if status_filter == 'follow_up': + q = q.filter( + Inspection.follow_up_required == True, + Inspection.status == 'completed', + ).filter(~Inspection.follow_ups.any()) + elif status_filter == 'has_issues': + from sqlalchemy import exists as sa_exists + q = q.filter(sa_exists().where(Issue.inspection_id == Inspection.id)) + elif status_filter: + q = q.filter(Inspection.status == status_filter) + if contract_filter.isdigit(): + _contract_fids = [ + f.id for f in Facility.query.filter_by(project_id=int(contract_filter)).all() + ] + q = q.filter(Inspection.facility_id.in_(_contract_fids)) if _contract_fids else q.filter(False) + if facility_filter.isdigit(): + q = q.filter(Inspection.facility_id == int(facility_filter)) + if date_from_filter: + try: + q = q.filter(Inspection.inspection_date >= datetime.strptime(date_from_filter, '%Y-%m-%d')) + except ValueError: + pass + if date_to_filter: + try: + _dt = datetime.strptime(date_to_filter, '%Y-%m-%d').replace(hour=23, minute=59, second=59) + q = q.filter(Inspection.inspection_date <= _dt) + except ValueError: + pass + if score_min_filter: + try: + q = q.filter(Inspection.overall_score >= float(score_min_filter)) + except ValueError: + pass + if score_max_filter: + try: + q = q.filter(Inspection.overall_score <= float(score_max_filter)) + except ValueError: + pass + if inspector_filter.isdigit() and current_user.role != 'inspector': + q = q.filter(Inspection.inspector_id == int(inspector_filter)) + + inspections = q.all() + + # Build a human-readable filter summary for the PDF header + filter_parts = [] + if status_filter: + label = {'follow_up': 'Flagged Follow-up', 'has_issues': 'Has Issues'}.get( + status_filter, status_filter.replace('_', ' ').title() + ) + filter_parts.append(f'Status: {label}') + if contract_filter.isdigit(): + p = db.session.get(Project, int(contract_filter)) + if p: + filter_parts.append(f'Contract: {p.name}') + if facility_filter.isdigit(): + f = db.session.get(Facility, int(facility_filter)) + if f: + filter_parts.append(f'Facility: {f.name}') + if date_from_filter: + filter_parts.append(f'From: {date_from_filter}') + if date_to_filter: + filter_parts.append(f'To: {date_to_filter}') + if score_min_filter: + filter_parts.append(f'Min score: {score_min_filter}%') + if score_max_filter: + filter_parts.append(f'Max score: {score_max_filter}%') + if inspector_filter.isdigit() and current_user.role != 'inspector': + u = db.session.get(User, int(inspector_filter)) + if u: + filter_parts.append(f'Inspector: {u.display_name}') + + filter_summary = ' | '.join(filter_parts) if filter_parts else 'All inspections' + + pdf_bytes = generate_inspections_list_pdf(inspections, filter_summary) + filename = f'inspections_list_{now_eastern().strftime("%Y%m%d_%H%M")}.pdf' + + log_action(ACTION_EXPORT, 'Inspection', None, 'Inspections List', + f'format=pdf; filters={filter_summary}; count={len(inspections)}') + + return Response( + pdf_bytes, + mimetype='application/pdf', + headers={'Content-Disposition': f'attachment; filename="{filename}"'}, + ) + + # ── Export to PDF ───────────────────────────────────────────────────────────── @bp.route('//export-pdf') diff --git a/app/templates/inspections/list.html b/app/templates/inspections/list.html index a9ef173..f7cef78 100644 --- a/app/templates/inspections/list.html +++ b/app/templates/inspections/list.html @@ -79,9 +79,14 @@ min="0" max="100" placeholder="100" value="{{ score_max_filter }}"> -
+
@@ -136,9 +141,9 @@ {% if ins.status == 'in_progress' or ins.status == 'flagged' %} - Continue + Continue {% else %} - View + View {% endif %} {% if current_user.role in ['admin', 'director'] %}