formatting improvements.

This commit is contained in:
2025-11-18 23:11:14 -08:00
parent 5524d7308c
commit 0bb071f337
6 changed files with 660 additions and 407 deletions

View File

@@ -27,18 +27,37 @@ rothbard/
├── generate_sample.py # Utility to generate sample Filevine API responses ├── generate_sample.py # Utility to generate sample Filevine API responses
├── requirements.txt # Python dependencies ├── requirements.txt # Python dependencies
├── rothbard-service-account.json # Firebase service account credentials ├── rothbard-service-account.json # Firebase service account credentials
├── rothbard-staging2-12345-firebase-adminsdk-fbsvc-7f95268383.json # Firebase service account for staging
├── static/ ├── static/
│ └── auth.js # Client-side authentication handling │ └── auth.js # Client-side authentication handling
├── templates/ # Jinja2 HTML templates ├── templates/ # Jinja2 HTML templates
│ ├── base.html # Base template with navigation │ ├── base.html # Base template with navigation
│ ├── login.html # Firebase login page │ ├── login.html # Firebase login page
│ ├── welcome.html # User welcome/onboarding page │ ├── welcome.html # User welcome/onboarding page
── dashboard.html # Main case dashboard ── dashboard.html # Main case dashboard
│ ├── admin_users.html # Admin user management interface
│ ├── admin_user_edit.html # Admin user edit interface
│ ├── admin_user_create.html # Admin user creation interface
│ └── _pagination.html # Pagination component
├── examples/ # Sample Filevine API responses ├── examples/ # Sample Filevine API responses
│ ├── project_list.json │ ├── forms__complaintInfo.json
│ ├── project_contacts.json │ ├── forms__newFileReview.json
── client.json ── project_tasks.json
└── .env # Environment variables (not tracked) │ ├── project_type_pahe_list.json
│ ├── project_team.json
│ └── ...
├── .env # Environment variables (not tracked)
├── .gcloudignore # Files ignored by Google Cloud Build
├── .gitignore # Files ignored by Git
├── cloudbuild.yaml # Google Cloud Build configuration
├── Dockerfile # Containerization configuration
├── deploy.sh # Deployment script
├── main.tf # Terraform infrastructure as code
├── firestore.rules # Firestore security rules
├── column_mapping.json # Column mapping configuration (unused)
├── DEPLOY.md # Deployment instructions
├── CLAUDE.md # Development guidance
└── utils.py # Utility functions for user profile management
``` ```
## Core Features ## Core Features
@@ -48,6 +67,7 @@ rothbard/
- Server-side session management with 8-hour expiration - Server-side session management with 8-hour expiration
- User profile management in Firestore - User profile management in Firestore
- Role-based access control (admin-enabled users only) - Role-based access control (admin-enabled users only)
- Admin interface for user management
### Case Management Dashboard ### Case Management Dashboard
- Real-time fetching of projects from Filevine API - Real-time fetching of projects from Filevine API
@@ -57,6 +77,9 @@ rothbard/
- Project numbers and incident dates - Project numbers and incident dates
- Contact information and project URLs - Contact information and project URLs
- Responsive design using Tailwind CSS - Responsive design using Tailwind CSS
- Configurable column visibility with local storage
- Pagination for large datasets
- Admin simulation mode to view other users' cases
### API Integration ### API Integration
- OAuth 2.0 authentication with Filevine API - OAuth 2.0 authentication with Filevine API
@@ -66,6 +89,16 @@ rothbard/
- Individual project details - Individual project details
- Client information - Client information
- Project contacts - Project contacts
- Project tasks and forms
### Admin Interface
- Comprehensive user management dashboard
- Enable/disable user access
- Grant/revoke admin privileges
- Reset user passwords
- Create new users
- View all user profiles
- Simulate case email access for testing
## Configuration ## Configuration
@@ -78,11 +111,17 @@ Create a `.env` file with the following variables:
FLASK_SECRET_KEY=your-secret-key-here FLASK_SECRET_KEY=your-secret-key-here
# Firebase Configuration # Firebase Configuration
# Choose ONE of these approaches:
# 1) Path to JSON creds file
GOOGLE_APPLICATION_CREDENTIALS=./rothbard-staging2-12345-firebase-adminsdk-fbsvc-7f95268383.json
# 2) Or inline JSON (escaped as single line)
# FIREBASE_SERVICE_ACCOUNT_JSON="{\"type\":\"service_account\",...}"
# Front-end Firebase (public — safe to expose)
FIREBASE_API_KEY=your-firebase-api-key FIREBASE_API_KEY=your-firebase-api-key
FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
FIREBASE_PROJECT_ID=your-firebase-project-id FIREBASE_PROJECT_ID=your-firebase-project-id
FIREBASE_APP_ID=your-firebase-app-id FIREBASE_APP_ID=your-firebase-app-id
FIREBASE_SERVICE_ACCOUNT_JSON='{"type":"service_account",...}' # or set GOOGLE_APPLICATION_CREDENTIALS
# Filevine API Configuration # Filevine API Configuration
FILEVINE_CLIENT_ID=your-filevine-client-id FILEVINE_CLIENT_ID=your-filevine-client-id
@@ -97,8 +136,9 @@ FILEVINE_USER_ID=your-filevine-user-id
1. Create a Firebase project at https://console.firebase.google.com 1. Create a Firebase project at https://console.firebase.google.com
2. Enable Authentication with Email/Password provider 2. Enable Authentication with Email/Password provider
3. Create a Firestore database 3. Create a Firestore database
4. Generate a service account key and save as `rothbard-service-account.json` 4. Generate a service account key and save as `rothbard-staging2-12345-firebase-adminsdk-fbsvc-7f95268383.json`
5. Configure Authentication settings for your web app 5. Configure Authentication settings for your web app
6. Set up Firestore security rules (provided in `firestore.rules`)
### Filevine API Setup ### Filevine API Setup
@@ -133,7 +173,7 @@ FILEVINE_USER_ID=your-filevine-user-id
``` ```
5. **Initialize Firebase** 5. **Initialize Firebase**
- Place your service account JSON file at `rothbard-service-account.json` - Place your service account JSON file at `rothbard-staging2-12345-firebase-adminsdk-fbsvc-7f95268383.json`
- Or set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable - Or set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable
6. **Run the application** 6. **Run the application**
@@ -158,6 +198,8 @@ FILEVINE_USER_ID=your-filevine-user-id
1. User profiles are stored in Firestore at `users/{uid}` 1. User profiles are stored in Firestore at `users/{uid}`
2. Enable users by setting `enabled: true` and providing a `caseEmail` 2. Enable users by setting `enabled: true` and providing a `caseEmail`
3. The `caseEmail` field determines which projects the user can access 3. The `caseEmail` field determines which projects the user can access
4. Admins can access the admin interface at `/admin/users` to manage all users
5. Admins can simulate other users' case access by entering their email in the simulation field on the dashboard
## Development Tools ## Development Tools
@@ -169,9 +211,10 @@ python generate_sample.py
``` ```
This will create JSON files in the `examples/` directory containing: This will create JSON files in the `examples/` directory containing:
- Sample project lists - Sample project forms (complaintInfo, newFileReview)
- Project contacts - Project tasks
- Client information - Project type phases
- Project team members
These samples are useful for development and testing without hitting the live API. These samples are useful for development and testing without hitting the live API.
@@ -183,6 +226,8 @@ These samples are useful for development and testing without hitting the live AP
- User access is controlled through Firestore profiles - User access is controlled through Firestore profiles
- Sensitive credentials are stored in environment variables - Sensitive credentials are stored in environment variables
- Filevine API tokens are properly scoped and managed - Filevine API tokens are properly scoped and managed
- Firestore security rules restrict access to user's own profile
- Google Cloud Build and deployment use secure practices
## Future Enhancements ## Future Enhancements
@@ -204,6 +249,8 @@ Planned improvements include:
- **python-dotenv 1.0.1** - Environment variable management - **python-dotenv 1.0.1** - Environment variable management
- **requests 2.32.3** - HTTP client for API calls - **requests 2.32.3** - HTTP client for API calls
- **itsdangerous 2.2.0** - Security utilities for Flask - **itsdangerous 2.2.0** - Security utilities for Flask
- **gunicorn 23.0.0** - WSGI HTTP Server
- **pytz 2024.1** - Timezone library
## API Endpoints ## API Endpoints
@@ -214,13 +261,21 @@ Planned improvements include:
- `GET /logout` - Session termination - `GET /logout` - Session termination
- `GET /welcome` - User onboarding page - `GET /welcome` - User onboarding page
- `GET /dashboard` - Main case dashboard (authenticated users only) - `GET /dashboard` - Main case dashboard (authenticated users only)
- `GET /admin/users` - Admin user management interface
- `GET /admin/users/<uid>` - Admin user detail view
- `POST /admin/users/update` - Update user information
- `POST /admin/users/create` - Create new user
- `POST /admin/users/<uid>/reset-password` - Reset user password
### Filevine API Integration ### Filevine API Integration
- Projects: `/fv-app/v2/Projects` - Projects: `/fv-app/v2/Projects`
- Project Details: `/fv-app/v2/Projects/{id}` - Project Details: `/fv-app/v2/Projects/{id}`
- Contacts: `/fv-app/v2/projects/{id}/contacts` - Project Team: `/fv-app/v2/Projects/{id}/team`
- Project Tasks: `/fv-app/v2/Projects/{id}/tasks`
- Project Forms: `/fv-app/v2/Projects/{id}/Forms/{form}`
- Project Contacts: `/fv-app/v2/projects/{id}/contacts`
- Client Info: `/fv-app/v2/contacts/{id}` - Client Info: `/fv-app/v2/contacts/{id}`
## License ## License
This project is proprietary software for Rothbard Law Group. This project is proprietary software for Rothbard Law Group.

43
app.py
View File

@@ -1,61 +1,20 @@
import json
import os import os
from functools import wraps from functools import wraps
from datetime import datetime, timedelta from datetime import datetime, timedelta
import pytz
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
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() load_dotenv()
import requests
from filevine_client import FilevineClient from filevine_client import FilevineClient
from utils import get_user_profile 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
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.environ.get("FLASK_SECRET_KEY", os.urandom(32)) app.secret_key = os.environ.get("FLASK_SECRET_KEY", os.urandom(32))
# --- Firebase Admin init --- (moved to firebase_init.py)
# --- Filevine env ---
FV_CLIENT_ID = os.environ.get("FILEVINE_CLIENT_ID")
FV_CLIENT_SECRET = os.environ.get("FILEVINE_CLIENT_SECRET")
FV_PAT = os.environ.get("FILEVINE_PERSONAL_ACCESS_TOKEN")
FV_ORG_ID = os.environ.get("FILEVINE_ORG_ID")
FV_USER_ID = os.environ.get("FILEVINE_USER_ID")
if not all([FV_CLIENT_ID, FV_CLIENT_SECRET, FV_PAT, FV_ORG_ID, FV_USER_ID]):
print("[WARN] Missing one or more Filevine env vars — dashboard will fail until set.")
PHASES = {
209436: "Nonpayment File Review",
209437: "Attorney File Review",
209438: "Notice Preparation",
209439: "Notice Pending",
209440: "Notice Expired",
209442: "Preparing and Filing UD",
209443: "Waiting for Answer",
209444: "Archived",
210761: "Service of Process",
211435: "Default",
211436: "Pre-Answer Motion",
211437: "Request for Trial",
211438: "Trial Prep and Trial",
211439: "Writ and Sheriff",
211440: "Lockout Pending",
211441: "Stipulation Preparation",
211442: "Stipulation Pending",
211443: "Stipulation Expired",
211446: "On Hold",
211466: "Request for Monetary Judgment",
211467: "Appeals and Post-Poss. Motions",
211957: "Migrated",
213691: "Close Out/ Invoicing",
213774: "Judgment After Stip & Order",
}
# --- Helpers --- # --- Helpers ---
def login_required(view): def login_required(view):

38
config.py Normal file
View File

@@ -0,0 +1,38 @@
import os
# --- Filevine env ---
FV_CLIENT_ID = os.environ.get("FILEVINE_CLIENT_ID")
FV_CLIENT_SECRET = os.environ.get("FILEVINE_CLIENT_SECRET")
FV_PAT = os.environ.get("FILEVINE_PERSONAL_ACCESS_TOKEN")
FV_ORG_ID = os.environ.get("FILEVINE_ORG_ID")
FV_USER_ID = os.environ.get("FILEVINE_USER_ID")
if not all([FV_CLIENT_ID, FV_CLIENT_SECRET, FV_PAT, FV_ORG_ID, FV_USER_ID]):
print("[WARN] Missing one or more Filevine env vars — dashboard will fail until set.")
PHASES = {
209436: "Nonpayment File Review",
209437: "Attorney File Review",
209438: "Notice Preparation",
209439: "Notice Pending",
209440: "Notice Expired",
209442: "Preparing and Filing UD",
209443: "Waiting for Answer",
209444: "Archived",
210761: "Service of Process",
211435: "Default",
211436: "Pre-Answer Motion",
211437: "Request for Trial",
211438: "Trial Prep and Trial",
211439: "Writ and Sheriff",
211440: "Lockout Pending",
211441: "Stipulation Preparation",
211442: "Stipulation Pending",
211443: "Stipulation Expired",
211446: "On Hold",
211466: "Request for Monetary Judgment",
211467: "Appeals and Post-Poss. Motions",
211957: "Migrated",
213691: "Close Out/ Invoicing",
213774: "Judgment After Stip & Order",
}

View File

@@ -40,7 +40,7 @@ def convert_to_pacific_time(date_str):
pacific_time = utc_time.astimezone(pytz.timezone('America/Los_Angeles')) pacific_time = utc_time.astimezone(pytz.timezone('America/Los_Angeles'))
# Format as YYYY-MM-DD # Format as YYYY-MM-DD
return pacific_time.strftime('%Y-%m-%d') return pacific_time.strftime('%d/%m/%Y')
except (ValueError, AttributeError) as e: except (ValueError, AttributeError) as e:
print(f"[WARN] Date conversion failed for '{date_str}': {e}") print(f"[WARN] Date conversion failed for '{date_str}': {e}")
return '' return ''

6
templates/_expander.html Normal file
View File

@@ -0,0 +1,6 @@
{% macro expander() %}
<div x-ref="content" class="content relative overflow-hidden transition-all duration-500"
:class="expanded ? 'max-h-100' : 'max-h-[3em]'">
{{ caller() }}
<div>
{% endmacro %}

View File

@@ -2,34 +2,29 @@
{% 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>
{% set profile = get_user_profile(session.uid) %} {% set profile = get_user_profile(session.uid) %}
{% if profile.is_admin %} {% if profile.is_admin %}
<div class="mb-4 flex w-[400px]"> <div class="mb-4 flex w-[400px]">
<label for="simulateCaseEmail" class=" text-sm font-medium text-slate-700 mb-1">Simulate case email:</label> <label for="simulateCaseEmail" class=" text-sm font-medium text-slate-700 mb-1">Simulate case email:</label>
<input type="text" <input type="text" id="simulateCaseEmail" x-model="case_email_sim"
id="simulateCaseEmail" @keyup.debounce.1000ms="window.location.href=`/dashboard/1?case_email=${encodeURIComponent($data.case_email_sim)}`"
x-model="case_email_sim" 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"
@keyup.debounce.1000ms="window.location.href=`/dashboard/1?case_email=${encodeURIComponent($data.case_email_sim)}`" placeholder="Enter case email to simulate">
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">
</div> </div>
{% endif %} {% endif %}
<!-- Configure Visible Columns Link --> <!-- Configure Visible Columns Link -->
<div class="mb-4"> <div class="mb-4">
<button @click="showColumnModal = true" <button @click="showColumnModal = true" class="text-blue-600 hover:text-blue-800 text-sm font-medium underline">
class="text-blue-600 hover:text-blue-800 text-sm font-medium underline">
Configure Visible Columns... Configure Visible Columns...
</button> </button>
</div> </div>
<!-- Column Configuration Modal --> <!-- Column Configuration Modal -->
<div x-show="showColumnModal" <div x-show="showColumnModal" x-cloak x-transition
x-cloak class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
x-transition @click.self="showColumnModal = false">
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
@click.self="showColumnModal = false">
<div class="bg-white rounded-lg p-6 max-w-2xl w-full max-h-[80vh] overflow-y-auto"> <div class="bg-white rounded-lg p-6 max-w-2xl w-full max-h-[80vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Configure Visible Columns</h3> <h3 class="text-lg font-semibold">Configure Visible Columns</h3>
@@ -39,392 +34,592 @@
</svg> </svg>
</button> </button>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="flex items-center"> <label class="flex items-center">
<input type="checkbox" <input type="checkbox" x-model="selectAll" @change="toggleAllColumns()"
x-model="selectAll" class="mr-2 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
@change="toggleAllColumns()"
class="mr-2 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-sm font-medium">Select All / Deselect All</span> <span class="text-sm font-medium">Select All / Deselect All</span>
</label> </label>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-96 overflow-y-auto"> <div class="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-96 overflow-y-auto">
<template x-for="(column, index) in columns" :key="index"> <template x-for="(column, index) in columns" :key="index">
<label class="flex items-center p-2 hover:bg-slate-50 rounded cursor-pointer"> <label class="flex items-center p-2 hover:bg-slate-50 rounded cursor-pointer">
<input type="checkbox" <input type="checkbox" :value="column" x-model="visibleColumns" @change="saveColumnSettings()"
:value="column" :checked="visibleColumns.includes(column)"
x-model="visibleColumns" class="mr-2 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
@change="saveColumnSettings()"
:checked="visibleColumns.includes(column)"
class="mr-2 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-sm" x-text="column"></span> <span class="text-sm" x-text="column"></span>
</label> </label>
</template> </template>
</div> </div>
<div class="flex justify-end space-x-3 mt-6"> <div class="flex justify-end space-x-3 mt-6">
<button @click="resetToDefault()" <button @click="resetToDefault()"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"> class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors">
Reset to Default Reset to Default
</button> </button>
<button @click="showColumnModal = false;" <button @click="showColumnModal = false;"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"> class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors">
Apply Changes Apply Changes
</button> </button>
</div> </div>
</div> </div>
</div> </div>
{% from "_expander.html" import expander %}
<div class="overflow-scroll"> <div class="overflow-scroll">
<table class="w-full whitespace-nowrap shadow-md border border-slate-200"> <table class="w-full table-fixed shadow-md border border-slate-200">
<thead class=" text-left text-sm sticky top-0 z-10 border-b border-blue-800 text-white font-medium" <thead class=" text-left text-sm z-10 border-b border-blue-800 text-white font-medium"
style="background-color: rgb(89, 121, 142);"> style="background-color: rgb(89, 121, 142);">
<tr> <tr>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Matter Num')}">Matter Num</th> <th class="px-4 py-3 w-16 sticky left-0 top-0 z-[40]" style="background-color: rgb(89, 121, 142);"></th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Client / Property')}">Client / Property</th> <th class="px-4 py-3 w-32 sticky left-16 top-0 z-[40]" style="background-color: rgb(89, 121, 142);" :class="{'hidden': !isColumnVisible('Matter Num')}">Matter Num</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Matter Description')}">Matter Description</th> <th class="px-4 py-3 w-64 sticky left-48 top-0 z-[40]" style="background-color: rgb(89, 121, 142);" :class="{'hidden': !isColumnVisible('Client / Property')}">Client / Property</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Defendant 1')}">Defendant 1</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-72 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Matter Description')}">Matter Description</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Matter Open')}">Matter Open</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-48 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Defendant 1')}">Defendant 1</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Practice Area')}">Practice Area</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Matter Open')}">Matter Open</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Notice Type')}">Notice Type</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Practice Area')}">Practice Area</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Case Number')}">Case Number</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-64 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Notice Type')}">Notice Type</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Premises Address')}">Premises Address</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Case Number')}">Case Number</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Premises City')}">Premises City</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-72 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Premises Address')}">Premises Address</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Assigned Attorney')}">Assigned Attorney</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isCstyle="background-color: rgb(89, 121, 142);" olumnVisible('Premises City')}">Premises City</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Primary Contact')}">Primary Contact</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-48 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Assigned Attorney')}">Assigned Attorney</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Secondary Paralegal')}">Secondary Paralegal</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-48 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Primary Contact')}">Primary Contact</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Documents')}">Documents</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-48 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Secondary Paralegal')}">Secondary Paralegal</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Matter Stage')}">Matter Stage</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Documents')}">Documents</th>
<th class="px-4 py-3 w-[400px]" :class="{'hidden': !isColumnVisible('Completed Tasks')}">Completed Tasks</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-64 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Matter Stage')}">Matter Stage</th>
<th class="px-4 py-3 w-[400px]" :class="{'hidden': !isColumnVisible('Pending Tasks')}">Pending Tasks</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-[400px] sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Completed Tasks')}">Completed Tasks</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Notice Service Date')}">Notice Service Date</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-[400px] sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Pending Tasks')}">Pending Tasks</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Notice Expir. Date')}">Notice Expir. Date</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Notice Service Date')}">Notice Service Date</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Date Case Filed')}">Date Case Filed</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Notice Expir. Date')}">Notice Expir. Date</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Daily Rent Damages')}">Daily Rent Damages</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Date Case Filed')}">Date Case Filed</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Default Date')}">Default Date</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Daily Rent Damages')}">Daily Rent Damages</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Default Entered On')}">Default Entered On</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Default Date')}">Default Date</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Motions:')}">Motions:</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Default Entered On')}">Default Entered On</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Demurrer Hearing Date')}">Demurrer Hearing Date</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Motions:')}">Motions:</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Motion To Strike Hearing Date')}">Motion To Strike Hearing Date</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Demurrer Hearing Date')}">Demurrer Hearing Date </th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Motion to Quash Hearing Date')}">Motion to Quash Hearing Date</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Motion To Strike Hearing Date')}">Motion To Strike Hearing Date</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Other Motion Hearing Date')}">Other Motion Hearing Date</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Motion to Quash Hearing Date')}">Motion to Quash Hearing Date</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('MSC Date')}">MSC Date</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Other Motion Hearing Date')}">Other Motion Hearing Date</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('MSC Time')}">MSC Time</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('MSC Date')}">MSC Date</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('MSC Address')}">MSC Address</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('MSC Time')}">MSC Time</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('MSC Div/ Dept/ Room')}">MSC Div/ Dept/ Room</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-72 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('MSC Address')}">MSC Address</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Trial Date')}">Trial Date</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-48 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('MSC Div/ Dept/ Room')}">MSC Div/ Dept/ Room</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Trial Time')}">Trial Time</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Trial Date')}">Trial Date</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Trial Address')}">Trial Address</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Trial Time')}">Trial Time</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Trial Div/ Dept/ Room')}">Trial Div/ Dept/ Room</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-72 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Trial Address')}">Trial Address</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Final Result of Trial/ MSC')}">Final Result of Trial/ MSC</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-48 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Trial Div/ Dept/ Room')}">Trial Div/ Dept/ Room </th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Date of Settlement')}">Date of Settlement</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-48 sticky top-0 z-[40]" :class="{'hidden': !isColumnVisible('Final Result of Trial/ MSC')}">Final Result of Trial/ MSC</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Final Obligation Under the Stip')}">Final Obligation Under the Stip</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0" :class="{'hidden': !isColumnVisible('Date of Settlement')}">Date of Settlement</th> <th class="px-4 py-3 w-32" :class="{'hidden': !isColumnVisible('Final Obligation Under the Stip')}">Final Obligation Under the Stip</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Def\'s Comply with the Stip?')}">Def's Comply with the Stip?</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0" :class="{'hidden': !isColumnVisible('Def\'s Comply with the Stip?')}">Def's Comply with the Stip?</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Judgment Date')}">Judgment Date</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0" :class="{'hidden': !isColumnVisible('Judgment Date')}">Judgment Date</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Writ Issued Date')}">Writ Issued Date</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0" :class="{'hidden': !isColumnVisible('Writ Issued Date')}">Writ Issued Date</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Scheduled Lockout')}">Scheduled Lockout</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0" :class="{'hidden': !isColumnVisible('Scheduled Lockout')}">Scheduled Lockout</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Oppose Stays?')}">Oppose Stays?</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0" :class="{'hidden': !isColumnVisible('Oppose Stays?')}">Oppose Stays?</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Premises Safety or Access Issues')}">Premises Safety or Access Issues</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-64 sticky top-0" :class="{'hidden': !isColumnVisible('Premises Safety or Access Issues')}">Premises Safety or Access Issues</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Matter Gate or Entry Code')}">Matter Gate or Entry Code</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-64 sticky top-0" :class="{'hidden': !isColumnVisible('Matter Gate or Entry Code')}">Matter Gate or Entry Code</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Date Possession Recovered')}">Date Possession Recovered</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0" :class="{'hidden': !isColumnVisible('Date Possession Recovered')}">Date Possession Recovered</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Attorney\'s Fees')}">Attorney's Fees</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0" :class="{'hidden': !isColumnVisible('Attorney\'s Fees')}">Attorney's Fees</th>
<th class="px-4 py-3" :class="{'hidden': !isColumnVisible('Costs')}">Costs</th> <th style="background-color: rgb(89, 121, 142);" class="px-4 py-3 w-32 sticky top-0" :class="{'hidden': !isColumnVisible('Costs')}">Costs</th>
</tr> </tr>
</thead> </thead>
<tbody class="bg-slate-100 divide-y divide-slate-300"> <tbody class="bg-slate-100 divide-y divide-slate-300">
{% for r in rows %} {% for r in rows %}
<tr class="hover:bg-slate-200 transition-colors duration-150 ease-in-out"> <tr class="hover:bg-slate-200 transition-colors duration-150 ease-in-out group"
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Matter Num')}">{{ r.number }}</td> x-data="{ expanded: false, hasOverflow: false }" x-init="
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Client / Property')}">{{ r.client }}</td> $nextTick(() => {
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Matter Description')}">{{ r.matter_description }}</td> // Check overflow for ANY td in this row
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Defendant 1')}">{{ r.defendant_1 }}</td> const tds = $el.querySelectorAll('td .content');
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Matter Open')}">{{ r.matter_open }}</td> hasOverflow = Array.from(tds).some(td => td.scrollHeight > td.clientHeight + 1);
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Practice Area')}"></td> });
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Notice Type')}">{{ r.notice_type }}</td> ">
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Case Number')}">{{ r.case_number }}</td> <td class="px-4 py-3 sticky left-0 bg-slate-100 z-[30] group-hover:bg-slate-200 ease-in-out duration-150 transition-colors">
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Premises Address')}">{{ r.premises_address }}</td> <button x-show="hasOverflow" x-on:click="expanded = !expanded"
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Premises City')}">{{ r.premises_city }}</td> class="mt-1 h-5 w-5 flex items-center justify-center rounded border cursor-pointer text-xs">
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Assigned Attorney')}"> <span class="inline-block transition-transform duration-150" :class="expanded ? 'rotate-90' : ''"></span>
</button>
</td>
<td class="px-4 py-3 text-sm text-slate-800 left-16 bg-slate-100 sticky z-[30] group-hover:bg-slate-200 ease-in-out duration-150 transition-colors" :class="{'hidden': !isColumnVisible('Matter Num')}">
{{ r.number }}
</td>
<td class="px-4 py-3 text-sm text-slate-800 sticky bg-slate-100 left-48 w-64 z-[30] group-hover:bg-slate-200 ease-in-out duration-150 transition-colors" :class="{'hidden': !isColumnVisible('Client / Property')}">
{% call expander() %}
{{ r.client }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm text-slate-800 w-64"
:class="{'hidden': !isColumnVisible('Matter Description')}">
{% call expander() %}
{{ r.matter_description }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Defendant 1')}">
{% call expander() %}
{{ r.defendant_1 }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Matter Open')}">
{% call expander() %}
{{ r.matter_open }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Practice Area')}">
{% call expander() %}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Notice Type')}">
{% call expander() %}
{{ r.notice_type }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Case Number')}">
{% call expander() %}
{{ r.case_number }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm text-slate-800 w-[200px]" :class="{'hidden': !isColumnVisible('Premises Address')}">
{% call expander() %}
{{ r.premises_address }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Premises City')}">
{% call expander() %}
{{ r.premises_city }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Assigned Attorney')}">
{% call expander() %}
{{ r.responsible_attorney }} {{ r.responsible_attorney }}
</td> {% endcall %}
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Primary Contact')}"> </td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Primary Contact')}">
{% call expander() %}
{{ r.staff_person }} {{ r.staff_person }}
</td> {% endcall %}
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Secondary Paralegal')}"> </td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Secondary Paralegal')}">
{% call expander() %}
{{ r.staff_person_2 }} {{ r.staff_person_2 }}
</td> {% endcall %}
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Documents')}"> </td>
{% if r.documents_url %} <td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Documents')}">
<a href="{{ r.documents_url }}">Documents</a>
{% endif %}</td> {% call expander() %}
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Matter Stage')}"> {{ r.phase_name }}</td> {% endcall %}
<td class="px-4 py-3 text-sm align-top whitespace-normal w-[400px]" :class="{'hidden': !isColumnVisible('Completed Tasks')}" x-data="{ showCompletedModal: false}"> {% if r.documents_url %}
{% if r.completed_tasks %} <a class="text-blue-500 text-underline" href=" {{ r.documents_url }}">Documents</a>
<div>
<table class="w-full text-xs">
<thead>
<tr>
<th class="text-left pb-1">Task</th>
<th class="text-right pb-1">Completed</th>
</tr>
</thead>
<tbody>
{% for x in r.completed_tasks[:2] %}
<tr>
<td class="py-1 pr-2 break-words">{{ x.description }}</td>
<td class="py-1 text-right whitespace-nowrap">{{ x.completed }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if r.completed_tasks|length > 2 %}
<button @click="showCompletedModal = true" class="text-blue-500 hover:text-blue-700 text-sm mt-1">Show more...</button>
{% endif %} {% endif %}
</div> </td>
<td class="px-4 py-3 text-sm text-slate-800" :class="{'hidden': !isColumnVisible('Matter Stage')}">
<div x-show="showCompletedModal" {% call expander() %}
x-cloak {{ r.phase_name }}
x-transition {% endcall %}
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" </td>
@click.self="showCompletedModal = false"> <td class="px-4 py-3 text-sm align-top whitespace-normal w-[400px]"
<div class="bg-white rounded-lg p-6 max-w-lg w-full max-h-[80vh] overflow-y-auto"> :class="{'hidden': !isColumnVisible('Completed Tasks')}" x-data="{ showCompletedModal: false}">
<div class="flex justify-between items-center mb-4"> {% call expander() %}
<h3 class="text-lg font-semibold">Completed Tasks</h3> {% if r.completed_tasks %}
<button @click="showCompletedModal = false" class="text-gray-500 hover:text-gray-700"> <div>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <table class="w-full text-xs">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<table class="w-full">
<thead> <thead>
<tr class="border-b"> <tr>
<th class="text-left pb-2">Task Description</th> <th class="text-left pb-1">Task</th>
<th class="text-right pb-2">Completed Date</th> <th class="text-right pb-1">Completed</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for x in r.completed_tasks %} {% for x in r.completed_tasks[:2] %}
<tr class="border-b border-slate-200"> <tr>
<td class="py-2 break-words">{{ x.description }}</td> <td class="py-1 pr-2 break-words">{{ x.description }}</td>
<td class="py-2 text-right whitespace-nowrap">{{ x.completed }}</td> <td class="py-1 text-right whitespace-nowrap">{{ x.completed }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if r.completed_tasks|length > 2 %}
<button @click="showCompletedModal = true" class="text-blue-500 hover:text-blue-700 text-sm mt-1">Show
more...</button>
{% endif %}
</div> </div>
</div>
{% endif %} <div x-show="showCompletedModal" x-cloak x-transition
</td> class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
<td class="px-4 py-3 text-sm align-top whitespace-normal w-[400px] min-w-[400px]" :class="{'hidden': !isColumnVisible('Pending Tasks')}" x-data="{ showPendingModal: false }"> @click.self="showCompletedModal = false">
{% if r.pending_tasks %} <div class="bg-white rounded-lg p-6 max-w-lg w-full max-h-[80vh] overflow-y-auto">
<div> <div class="flex justify-between items-center mb-4">
<table class="w-full text-xs"> <h3 class="text-lg font-semibold">Completed Tasks</h3>
<thead> <button @click="showCompletedModal = false" class="text-gray-500 hover:text-gray-700">
<tr> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<th class="text-left pb-1">Task</th> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12">
</tr> </path>
</thead> </svg>
<tbody> </button>
{% for x in r.pending_tasks[:2] %} </div>
<tr> <table class="w-full">
<td class="py-1 break-words">{{ x.description }}</td> <thead>
</tr> <tr class="border-b">
{% endfor %} <th class="text-left pb-2">Task Description</th>
</tbody> <th class="text-right pb-2">Completed Date</th>
</table> </tr>
{% if r.pending_tasks|length > 2 %} </thead>
<button @click="showPendingModal = true" class="text-blue-500 hover:text-blue-700 text-sm mt-1">Show more...</button> <tbody>
{% endif %} {% for x in r.completed_tasks %}
</div> <tr class="border-b border-slate-200">
<td class="py-2 break-words">{{ x.description }}</td>
<div x-show="showPendingModal" <td class="py-2 text-right whitespace-nowrap">{{ x.completed }}</td>
x-cloak </tr>
x-transition {% endfor %}
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" </tbody>
@click.self="showPendingModal = false"> </table>
<div class="bg-white rounded-lg p-6 max-w-lg w-full max-h-[80vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Pending Tasks</h3>
<button @click="showPendingModal = false" class="text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div> </div>
<table class="w-full"> </div>
{% endif %}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm align-top whitespace-normal w-[400px] min-w-[400px]"
:class="{'hidden': !isColumnVisible('Pending Tasks')}" x-data="{ showPendingModal: false }">
{% call expander() %}
{% if r.pending_tasks %}
<div>
<table class="w-full text-xs">
<thead> <thead>
<tr class="border-b"> <tr>
<th class="text-left pb-2">Task Description</th> <th class="text-left pb-1">Task</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for x in r.pending_tasks %} {% for x in r.pending_tasks[:2] %}
<tr class="border-b border-slate-200"> <tr>
<td class="py-2 break-words">{{ x.description }}</td> <td class="py-1 break-words">{{ x.description }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if r.pending_tasks|length > 2 %}
<button @click="showPendingModal = true" class="text-blue-500 hover:text-blue-700 text-sm mt-1">Show
more...</button>
{% endif %}
</div> </div>
</div>
{% endif %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Notice Service Date')}">{{ r.notice_service_date }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Notice Expir. Date')}">{{ r.notice_expiration_date }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Date Case Filed')}">{{ r.case_field_date }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Daily Rent Damages')}">{% if r.daily_rent_damages %}${{ "{:,.2f}".format(r.daily_rent_damages) }}{% endif %}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Default Date')}">{{ r.default_date }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Default Entered On')}">{{ r.default_entered_on_date }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Motions:')}"></td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Demurrer Hearing Date')}">{{ r.demurrer_hearing_date }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Motion To Strike Hearing Date')}">{{ r.motion_to_strike_hearing_date }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Motion to Quash Hearing Date')}">{{ r.motion_to_quash_hearing_date }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Other Motion Hearing Date')}">{{ r.other_motion_hearing_date }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('MSC Date')}">{{ r.msc_date }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('MSC Time')}">{{ r.msc_time }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('MSC Address')}">{{ r.msc_address }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('MSC Div/ Dept/ Room')}">{{ r.msc_div_dept_room }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Trial Date')}">{{ r.trial_date }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Trial Time')}">{{ r.trial_time }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Trial Address')}">{{ r.trial_address }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Trial Div/ Dept/ Room')}">{{ r.trial_div_dept_room }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Final Result of Trial/ MSC')}">{{ r.final_result }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Date of Settlement')}">{{ r.date_of_settlement }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Final Obligation Under the Stip')}">{{ r.final_obligation }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Def\'s Comply with the Stip?')}">{{ r.def_comply_stip }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Judgment Date')}">{{ r.judgment_date }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Writ Issued Date')}">{{ r.writ_issued_date }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Scheduled Lockout')}">{{ r.scheduled_lockout }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Oppose Stays?')}">{{ r.oppose_stays }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Premises Safety or Access Issues')}">{{ r.premises_safety }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Matter Gate or Entry Code')}">{{ r.matter_gate_code }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Date Possession Recovered')}">{{ r.date_possession_recovered }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Attorney\'s Fees')}">{{ r.attorney_fees }}</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Costs')}">{{ r.costs }}</td>
</tr>
{% else %}
<tr>
<td colspan="53" class="px-4 py-6 text-center text-slate-500">No matching projects found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination --> <div x-show="showPendingModal" x-cloak x-transition
{% include '_pagination.html' %} class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
@click.self="showPendingModal = false">
<div class="bg-white rounded-lg p-6 max-w-lg w-full max-h-[80vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Pending Tasks</h3>
<button @click="showPendingModal = false" class="text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12">
</path>
</svg>
</button>
</div>
<table class="w-full">
<thead>
<tr class="border-b">
<th class="text-left pb-2">Task Description</th>
</tr>
</thead>
<tbody>
{% for x in r.pending_tasks %}
<tr class="border-b border-slate-200">
<td class="py-2 break-words">{{ x.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Notice Service Date')}">
{% call expander() %}
{{ r.notice_service_date }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Notice Expir. Date')}">
{% call expander() %}
{{ r.notice_expiration_date }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Date Case Filed')}">
<script> {% call expander() %}
function columnConfig() { {{ r.case_field_date }}
return { {% endcall %}
showColumnModal: false, </td>
case_email_sim: '', <td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Daily Rent Damages')}">
columns: [
'Matter Num', {% call expander() %}
'Client / Property', {% if r.daily_rent_damages %}${{ "{:,.2f}".format(r.daily_rent_damages) }}{% endif %}
'Matter Description', {% endcall %}
'Defendant 1', </td>
'Matter Open', <td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Default Date')}">
'Practice Area', {% call expander() %}
'Notice Type', {{ r.default_date }}
'Case Number', {% endcall %}
'Premises Address', </td>
'Premises City', <td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Default Entered On')}">
'Assigned Attorney', {% call expander() %}
'Primary Contact', {{ r.default_entered_on_date }}
'Secondary Paralegal', {% endcall %}
'Documents', </td>
'Matter Stage', <td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Motions:')}"></td>
'Completed Tasks', <td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Demurrer Hearing Date')}">
'Pending Tasks', {% call expander() %}
'Notice Service Date', {{ r.demurrer_hearing_date }}
'Notice Expir. Date', {% endcall %}
'Date Case Filed', </td>
'Daily Rent Damages', <td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Motion To Strike Hearing Date')}">
'Default Date', {% call expander() %}
'Default Entered On', {{ r.motion_to_strike_hearing_date }}
'Motions:', {% endcall %}
'Demurrer Hearing Date', </td>
'Motion To Strike Hearing Date', <td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Motion to Quash Hearing Date')}">
'Motion to Quash Hearing Date',
'Other Motion Hearing Date', {% call expander() %}
'MSC Date', {{ r.motion_to_quash_hearing_date }}
'MSC Time', {% endcall %}
'MSC Address', </td>
'MSC Div/ Dept/ Room', <td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Other Motion Hearing Date')}">
'Trial Date', {% call expander() %}
'Trial Time', {{ r.other_motion_hearing_date }}
'Trial Address', {% endcall %}
'Trial Div/ Dept/ Room', </td>
'Final Result of Trial/ MSC', <td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('MSC Date')}">
'Date of Settlement', {% call expander() %}
'Final Obligation Under the Stip', {{ r.msc_date }}
'Def\'s Comply with the Stip?', {% endcall %}
'Judgment Date', </td>
'Writ Issued Date', <td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('MSC Time')}">
'Scheduled Lockout', {% call expander() %}
'Oppose Stays?', {{ r.msc_time }}
'Premises Safety or Access Issues', {% endcall %}
'Matter Gate or Entry Code', </td>
'Date Possession Recovered', <td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('MSC Address')}">
'Attorney\'s Fees', {% call expander() %}
'Costs' {{ r.msc_address }}
], {% endcall %}
selectAll: true, </td>
visibleColumns: [], <td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('MSC Div/ Dept/ Room')}">
{% call expander() %}
init() { {{ r.msc_div_dept_room }}
this.loadColumnSettings(); {% endcall %}
</td>
// Extract case_email from URL query parameter <td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Trial Date')}">
const urlParams = new URLSearchParams(window.location.search);
const caseEmail = urlParams.get('case_email'); {% call expander() %}
if (caseEmail) { {{ r.trial_date }}
this.case_email_sim = caseEmail; {% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Trial Time')}">
{% call expander() %}
{{ r.trial_time }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Trial Address')}">
{% call expander() %}
{{ r.trial_address }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Trial Div/ Dept/ Room')}">
{% call expander() %}
{{ r.trial_div_dept_room }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Final Result of Trial/ MSC')}">
{% call expander() %}
{{ r.final_result }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Date of Settlement')}">
{% call expander() %}
{{ r.date_of_settlement }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Final Obligation Under the Stip')}">
{% call expander() %}
{{ r.final_obligation }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Def\'s Comply with the Stip?')}">
{% call expander() %}
{{ r.def_comply_stip }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Judgment Date')}">
{% call expander() %}
{{ r.judgment_date }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Writ Issued Date')}">
{% call expander() %}
{{ r.writ_issued_date }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Scheduled Lockout')}">
{% call expander() %}
{{ r.scheduled_lockout }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Oppose Stays?')}">
{% call expander() %}
{{ r.oppose_stays }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Premises Safety or Access Issues')}">
{% call expander() %}
{{ r.premises_safety }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Matter Gate or Entry Code')}">
{% call expander() %}
{{ r.matter_gate_code }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Date Possession Recovered')}">
{% call expander() %}
{{ r.date_possession_recovered }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Attorney\'s Fees')}">
{% call expander() %}
{{ r.attorney_fees }}
{% endcall %}
</td>
<td class="px-4 py-3 text-sm" :class="{'hidden': !isColumnVisible('Costs')}">
{% call expander() %}
{{ r.costs }}
{% endcall %}
</td>
</tr>
{% else %}
<tr>
<td colspan="53" class="px-4 py-6 text-center text-slate-500">No matching projects found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% include '_pagination.html' %}
<script>
function columnConfig() {
return {
showColumnModal: false,
case_email_sim: '',
columns: [
'Matter Num',
'Client / Property',
'Matter Description',
'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'
],
selectAll: true,
visibleColumns: [],
init() {
this.loadColumnSettings();
// Extract case_email from URL query parameter
const urlParams = new URLSearchParams(window.location.search);
const caseEmail = urlParams.get('case_email');
if (caseEmail) {
this.case_email_sim = caseEmail;
}
},
isColumnVisible(columnName) {
return this.visibleColumns.includes(columnName);
},
loadColumnSettings() {
const stored = localStorage.getItem('dashboardColumnVisibility');
if (stored) {
this.visibleColumns = JSON.parse(stored);
this.selectAll = this.visibleColumns.length === this.columns.length;
} else {
// Default to showing all columns
this.visibleColumns = [...this.columns];
this.selectAll = true;
}
},
saveColumnSettings() {
localStorage.setItem('dashboardColumnVisibility', JSON.stringify(this.visibleColumns));
},
toggleAllColumns() {
if (this.visibleColumns.length === 0) {
this.visibleColumns = [...this.columns];
this.selectAll = true;
} else {
this.visibleColumns = [];
this.selectAll = false;
}
this.saveColumnSettings();
},
resetToDefault() {
this.visibleColumns = [...this.columns];
this.selectAll = true;
this.saveColumnSettings();
}
} }
},
isColumnVisible(columnName) {
return this.visibleColumns.includes(columnName);
},
loadColumnSettings() {
const stored = localStorage.getItem('dashboardColumnVisibility');
if (stored) {
this.visibleColumns = JSON.parse(stored);
this.selectAll = this.visibleColumns.length === this.columns.length;
} else {
// Default to showing all columns
this.visibleColumns = [...this.columns];
this.selectAll = true;
}
},
saveColumnSettings() {
localStorage.setItem('dashboardColumnVisibility', JSON.stringify(this.visibleColumns));
},
toggleAllColumns() {
if (this.selectAll) {
this.visibleColumns = [];
} else {
this.visibleColumns = [...this.columns];
}
this.selectAll = !this.selectAll;
this.saveColumnSettings();
},
resetToDefault() {
this.visibleColumns = [...this.columns];
this.selectAll = true;
this.saveColumnSettings();
} }
} </script>
}
</script>
</div> </div>
<!-- <!--