supports export

This commit is contained in:
2025-11-20 22:53:16 -08:00
parent 5afb05d261
commit aca18b781b
3 changed files with 358 additions and 66 deletions

277
app.py
View File

@@ -2,7 +2,7 @@ import os
from functools import wraps from functools import wraps
from datetime import datetime, timedelta 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 from dotenv import load_dotenv
load_dotenv() load_dotenv()
from filevine_client import FilevineClient from filevine_client import FilevineClient
@@ -10,6 +10,9 @@ from utils import get_user_profile
from firebase_init import db from firebase_init import db
from firebase_admin import auth as fb_auth from firebase_admin import auth as fb_auth
import config import config
import openpyxl
from openpyxl import Workbook
from openpyxl.utils import get_column_letter
app = Flask(__name__) app = Flask(__name__)
@@ -191,6 +194,278 @@ def dashboard(page=1):
total_projects=total_projects, total_projects=total_projects,
per_page=per_page) 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 import admin
# Register admin routes # Register admin routes

View File

@@ -5,3 +5,4 @@ requests==2.32.3
itsdangerous==2.2.0 itsdangerous==2.2.0
gunicorn==23.0.0 gunicorn==23.0.0
pytz pytz
openpyxl==3.1.2

View File

@@ -2,6 +2,7 @@
{% block content %} {% block content %}
<div class="h-full flex flex-col" x-data="columnConfig()"> <div class="h-full flex flex-col" x-data="columnConfig()">
<h1 class="text-xl font-semibold mb-4">Projects for {{ case_email }}</h1> <h1 class="text-xl font-semibold mb-4">Projects for {{ case_email }}</h1>
<div class="flex justify-between">
{% set profile = get_user_profile(session.uid) %} {% set profile = get_user_profile(session.uid) %}
{% if profile.is_admin %} {% if profile.is_admin %}
@@ -12,8 +13,21 @@
class="w-full px-3 py-2 border w-64 border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" class="w-full px-3 py-2 border w-64 border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter case email to simulate"> placeholder="Enter case email to simulate">
</div> </div>
{% else %}
<div></div>
{% endif %} {% endif %}
<div class="flex gap-2 items-center">
<!-- Export Button -->
<div class="mb-4">
<a href="{{ url_for('dashboard_export_xls') }}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="mr-2 -ml-1 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
Export to Excel
</a>
</div>
<!-- Per Page Dropdown --> <!-- Per Page Dropdown -->
<div class="mb-4 flex items-center"> <div class="mb-4 flex items-center">
<label for="perPage" class="mr-2 text-sm font-medium text-slate-700">Items per page:</label> <label for="perPage" class="mr-2 text-sm font-medium text-slate-700">Items per page:</label>
@@ -78,6 +92,8 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
{% from "_expander.html" import expander %} {% from "_expander.html" import expander %}
<div class="overflow-scroll"> <div class="overflow-scroll">