supports export
This commit is contained in:
277
app.py
277
app.py
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user