diff --git a/app.py b/app.py index dab4968..2198bcf 100644 --- a/app.py +++ b/app.py @@ -2,7 +2,7 @@ import os from functools import wraps from datetime import datetime, timedelta -from flask import Flask, render_template, request, redirect, url_for, session, abort, jsonify +from flask import Flask, render_template, request, redirect, url_for, session, abort, jsonify, send_file from dotenv import load_dotenv load_dotenv() from filevine_client import FilevineClient @@ -10,6 +10,9 @@ from utils import get_user_profile from firebase_init import db from firebase_admin import auth as fb_auth import config +import openpyxl +from openpyxl import Workbook +from openpyxl.utils import get_column_letter app = Flask(__name__) @@ -191,6 +194,278 @@ def dashboard(page=1): total_projects=total_projects, per_page=per_page) + +@app.route("/dashboard/export_xls") +@login_required +def dashboard_export_xls(): + """Export all dashboard data to XLS format without pagination.""" + uid = session.get("uid") + profile = get_user_profile(uid) + if not profile.get("enabled"): + return redirect(url_for("welcome")) + + is_admin = profile.get("is_admin") + case_email = None + if not is_admin: + case_email = profile.get("case_email") + if not case_email: + return redirect(url_for("welcome")) + if is_admin and request.args.get('case_email'): + case_email = request.args.get('case_email').lower() + # Validate email format + if '@' not in case_email: + return abort(400, "Invalid email format") + + # Get all projects without pagination + try: + projects_ref = db.collection("projects") + # Filter projects where case_email is in viewing_emails array + if case_email: + projects_ref = projects_ref.where("viewing_emails", "array_contains", case_email.lower()) + + # Order by matter_description to maintain consistent ordering + projects_ref = projects_ref.order_by("matter_description") + + docs = projects_ref.stream() + all_rows = [] + + for doc in docs: + all_rows.append(doc.to_dict()) + + print(f"Retrieved {len(all_rows)} projects from Firestore for XLS export") + + # Create workbook and worksheet + wb = Workbook() + ws = wb.active + ws.title = "Projects" + + # Define the column headers (matching the dashboard columns) + headers = [ + 'Matter Num', + 'Matter Description', + 'Client / Property', + 'Defendant 1', + 'Matter Open', + 'Practice Area', + 'Notice Type', + 'Case Number', + 'Premises Address', + 'Premises City', + 'Assigned Attorney', + 'Primary Contact', + 'Secondary Paralegal', + 'Documents', + 'Matter Stage', + 'Completed Tasks', + 'Pending Tasks', + 'Notice Service Date', + 'Notice Expir. Date', + 'Date Case Filed', + 'Daily Rent Damages', + 'Default Date', + 'Default Entered On', + 'Motions:', + 'Demurrer Hearing Date', + 'Motion To Strike Hearing Date', + 'Motion to Quash Hearing Date', + 'Other Motion Hearing Date', + 'MSC Date', + 'MSC Time', + 'MSC Address', + 'MSC Div/ Dept/ Room', + 'Trial Date', + 'Trial Time', + 'Trial Address', + 'Trial Div/ Dept/ Room', + 'Final Result of Trial/ MSC', + 'Date of Settlement', + 'Final Obligation Under the Stip', + 'Def\'s Comply with the Stip?', + 'Judgment Date', + 'Writ Issued Date', + 'Scheduled Lockout', + 'Oppose Stays?', + 'Premises Safety or Access Issues', + 'Matter Gate or Entry Code', + 'Date Possession Recovered', + 'Attorney\'s Fees', + 'Costs' + ] + + # Write headers + for col_num, header in enumerate(headers, 1): + cell = ws.cell(row=1, column=col_num, value=header) + cell.font = openpyxl.styles.Font(bold=True) + + # Write data rows + for row_num, row_data in enumerate(all_rows, 2): + for col_num, header in enumerate(headers, 1): + # Map header names to data fields + field_name = None + if header == 'Matter Num': + field_name = 'number' + elif header == 'Matter Description': + field_name = 'matter_description' + elif header == 'Client / Property': + field_name = 'client' + elif header == 'Defendant 1': + field_name = 'defendant_1' + elif header == 'Matter Open': + field_name = 'matter_open' + elif header == 'Practice Area': + field_name = 'practice_area' # This field doesn't exist in model, so we'll leave blank + elif header == 'Notice Type': + field_name = 'notice_type' + elif header == 'Case Number': + field_name = 'case_number' + elif header == 'Premises Address': + field_name = 'premises_address' + elif header == 'Premises City': + field_name = 'premises_city' + elif header == 'Assigned Attorney': + field_name = 'responsible_attorney' + elif header == 'Primary Contact': + field_name = 'staff_person' + elif header == 'Secondary Paralegal': + field_name = 'staff_person_2' + elif header == 'Documents': + field_name = 'documents_url' # Just the URL, not the link + elif header == 'Matter Stage': + field_name = 'phase_name' + elif header == 'Completed Tasks': + # Convert list of dicts to string representation + if 'completed_tasks' in row_data and row_data['completed_tasks']: + task_strings = [] + for task in row_data['completed_tasks'][:5]: # Limit to 5 tasks for readability + desc = task.get('description', '') + completed = task.get('completed', '') + task_strings.append(f"{desc} ({completed})") + field_value = '; '.join(task_strings) + else: + field_value = '' + ws.cell(row=row_num, column=col_num, value=field_value) + continue # Skip to next field + elif header == 'Pending Tasks': + # Convert list of dicts to string representation + if 'pending_tasks' in row_data and row_data['pending_tasks']: + task_strings = [] + for task in row_data['pending_tasks'][:5]: # Limit to 5 tasks for readability + desc = task.get('description', '') + task_strings.append(desc) + field_value = '; '.join(task_strings) + else: + field_value = '' + ws.cell(row=row_num, column=col_num, value=field_value) + continue # Skip to next field + elif header == 'Notice Service Date': + field_name = 'notice_service_date' + elif header == 'Notice Expir. Date': + field_name = 'notice_expiration_date' + elif header == 'Date Case Filed': + field_name = 'case_field_date' + elif header == 'Daily Rent Damages': + field_name = 'daily_rent_damages' + elif header == 'Default Date': + field_name = 'default_date' + elif header == 'Default Entered On': + field_name = 'default_entered_on_date' + elif header == 'Motions:': + field_name = 'motions' # This field doesn't exist in model, so we'll leave blank + elif header == 'Demurrer Hearing Date': + field_name = 'demurrer_hearing_date' + elif header == 'Motion To Strike Hearing Date': + field_name = 'motion_to_strike_hearing_date' + elif header == 'Motion to Quash Hearing Date': + field_name = 'motion_to_quash_hearing_date' + elif header == 'Other Motion Hearing Date': + field_name = 'other_motion_hearing_date' + elif header == 'MSC Date': + field_name = 'msc_date' + elif header == 'MSC Time': + field_name = 'msc_time' + elif header == 'MSC Address': + field_name = 'msc_address' + elif header == 'MSC Div/ Dept/ Room': + field_name = 'msc_div_dept_room' + elif header == 'Trial Date': + field_name = 'trial_date' + elif header == 'Trial Time': + field_name = 'trial_time' + elif header == 'Trial Address': + field_name = 'trial_address' + elif header == 'Trial Div/ Dept/ Room': + field_name = 'trial_div_dept_room' + elif header == 'Final Result of Trial/ MSC': + field_name = 'final_result' + elif header == 'Date of Settlement': + field_name = 'date_of_settlement' + elif header == 'Final Obligation Under the Stip': + field_name = 'final_obligation' + elif header == 'Def\'s Comply with the Stip?': + field_name = 'def_comply_stip' + elif header == 'Judgment Date': + field_name = 'judgment_date' + elif header == 'Writ Issued Date': + field_name = 'writ_issued_date' + elif header == 'Scheduled Lockout': + field_name = 'scheduled_lockout' + elif header == 'Oppose Stays?': + field_name = 'oppose_stays' + elif header == 'Premises Safety or Access Issues': + field_name = 'premises_safety' + elif header == 'Matter Gate or Entry Code': + field_name = 'matter_gate_code' + elif header == 'Date Possession Recovered': + field_name = 'date_possession_recovered' + elif header == 'Attorney\'s Fees': + field_name = 'attorney_fees' + elif header == 'Costs': + field_name = 'costs' + + # Set the cell value + if field_name and field_name in row_data: + field_value = row_data[field_name] + # Handle special cases for formatting + if field_name == 'daily_rent_damages' and field_value: + # Format as currency if it's a number + try: + field_value = f"${float(field_value):,.2f}" + except: + pass # Keep as-is if not a number + ws.cell(row=row_num, column=col_num, value=field_value) + else: + # For fields that don't exist in the model or are special cases, set to empty + ws.cell(row=row_num, column=col_num, value='') + + # Auto-adjust column widths + for column in ws.columns: + max_length = 0 + column_letter = get_column_letter(column[0].column) + for cell in column: + try: + if len(str(cell.value)) > max_length: + max_length = len(str(cell.value)) + except: + pass + adjusted_width = min(max_length + 2, 50) # Cap at 50 characters + ws.column_dimensions[column_letter].width = adjusted_width + + # Save to temporary file and return + import tempfile + import os + temp_dir = tempfile.gettempdir() + filename = f"dashboard_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" + filepath = os.path.join(temp_dir, filename) + + wb.save(filepath) + + return send_file(filepath, as_attachment=True, download_name=filename) + + except Exception as e: + print(f"[ERROR] Failed to export XLS: {e}") + return abort(500, "Failed to export data") + + import admin # Register admin routes diff --git a/requirements.txt b/requirements.txt index 7677532..1fb615c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ requests==2.32.3 itsdangerous==2.2.0 gunicorn==23.0.0 pytz +openpyxl==3.1.2 diff --git a/templates/dashboard.html b/templates/dashboard.html index a00b11f..5502f20 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -2,80 +2,96 @@ {% block content %}