This commit is contained in:
HotSwapp
2025-08-18 20:20:04 -05:00
parent 89b2bc0aa2
commit bac8cc4bd5
114 changed files with 30258 additions and 1341 deletions

View File

@@ -9,9 +9,10 @@ from sqlalchemy.orm import Session, joinedload
from sqlalchemy import and_, func, or_, desc
from app.models import (
File, Ledger, FileStatus, FileType, Rolodex, Employee,
BillingStatement, Timer, TimeEntry, User, FileStatusHistory,
FileTransferHistory, FileArchiveInfo
File, Ledger, FileStatus, FileType, Rolodex, Employee,
BillingStatement, Timer, TimeEntry, User, FileStatusHistory,
FileTransferHistory, FileArchiveInfo, FileClosureChecklist, FileAlert,
FileRelationship
)
from app.utils.logging import app_logger
@@ -432,6 +433,284 @@ class FileManagementService:
logger.info(f"Bulk status update: {len(results['successful'])} successful, {len(results['failed'])} failed")
return results
# Checklist management
def get_closure_checklist(self, file_no: str) -> List[Dict[str, Any]]:
"""Return the closure checklist items for a file."""
items = self.db.query(FileClosureChecklist).filter(
FileClosureChecklist.file_no == file_no
).order_by(FileClosureChecklist.sort_order.asc(), FileClosureChecklist.id.asc()).all()
return [
{
"id": i.id,
"file_no": i.file_no,
"item_name": i.item_name,
"item_description": i.item_description,
"is_required": bool(i.is_required),
"is_completed": bool(i.is_completed),
"completed_date": i.completed_date,
"completed_by_name": i.completed_by_name,
"notes": i.notes,
"sort_order": i.sort_order,
}
for i in items
]
def add_checklist_item(
self,
*,
file_no: str,
item_name: str,
item_description: Optional[str] = None,
is_required: bool = True,
sort_order: int = 0,
) -> FileClosureChecklist:
"""Add a checklist item to a file."""
# Ensure file exists
if not self.db.query(File).filter(File.file_no == file_no).first():
raise FileManagementError(f"File {file_no} not found")
item = FileClosureChecklist(
file_no=file_no,
item_name=item_name,
item_description=item_description,
is_required=is_required,
sort_order=sort_order,
)
self.db.add(item)
self.db.commit()
self.db.refresh(item)
logger.info(f"Added checklist item '{item_name}' to file {file_no}")
return item
def update_checklist_item(
self,
*,
item_id: int,
item_name: Optional[str] = None,
item_description: Optional[str] = None,
is_required: Optional[bool] = None,
is_completed: Optional[bool] = None,
sort_order: Optional[int] = None,
user_id: Optional[int] = None,
notes: Optional[str] = None,
) -> FileClosureChecklist:
"""Update attributes of a checklist item; optionally mark complete/incomplete."""
item = self.db.query(FileClosureChecklist).filter(FileClosureChecklist.id == item_id).first()
if not item:
raise FileManagementError("Checklist item not found")
if item_name is not None:
item.item_name = item_name
if item_description is not None:
item.item_description = item_description
if is_required is not None:
item.is_required = bool(is_required)
if sort_order is not None:
item.sort_order = int(sort_order)
if is_completed is not None:
item.is_completed = bool(is_completed)
if item.is_completed:
item.completed_date = datetime.now(timezone.utc)
if user_id:
user = self.db.query(User).filter(User.id == user_id).first()
item.completed_by_user_id = user_id
item.completed_by_name = user.username if user else f"user_{user_id}"
else:
item.completed_date = None
item.completed_by_user_id = None
item.completed_by_name = None
if notes is not None:
item.notes = notes
self.db.commit()
self.db.refresh(item)
logger.info(f"Updated checklist item {item_id}")
return item
def delete_checklist_item(self, *, item_id: int) -> None:
item = self.db.query(FileClosureChecklist).filter(FileClosureChecklist.id == item_id).first()
if not item:
raise FileManagementError("Checklist item not found")
self.db.delete(item)
self.db.commit()
logger.info(f"Deleted checklist item {item_id}")
# Alerts management
def create_alert(
self,
*,
file_no: str,
alert_type: str,
title: str,
message: str,
alert_date: date,
notify_attorney: bool = True,
notify_admin: bool = False,
notification_days_advance: int = 7,
) -> FileAlert:
if not self.db.query(File).filter(File.file_no == file_no).first():
raise FileManagementError(f"File {file_no} not found")
alert = FileAlert(
file_no=file_no,
alert_type=alert_type,
title=title,
message=message,
alert_date=alert_date,
notify_attorney=notify_attorney,
notify_admin=notify_admin,
notification_days_advance=notification_days_advance,
)
self.db.add(alert)
self.db.commit()
self.db.refresh(alert)
logger.info(f"Created alert {alert.id} for file {file_no} on {alert_date}")
return alert
def get_alerts(
self,
*,
file_no: str,
active_only: bool = True,
upcoming_only: bool = False,
limit: int = 100,
) -> List[FileAlert]:
query = self.db.query(FileAlert).filter(FileAlert.file_no == file_no)
if active_only:
query = query.filter(FileAlert.is_active == True)
if upcoming_only:
today = datetime.now(timezone.utc).date()
query = query.filter(FileAlert.alert_date >= today)
return query.order_by(FileAlert.alert_date.asc(), FileAlert.id.asc()).limit(limit).all()
def acknowledge_alert(self, *, alert_id: int, user_id: int) -> FileAlert:
alert = self.db.query(FileAlert).filter(FileAlert.id == alert_id).first()
if not alert:
raise FileManagementError("Alert not found")
if not alert.is_active:
return alert
alert.is_acknowledged = True
alert.acknowledged_at = datetime.now(timezone.utc)
alert.acknowledged_by_user_id = user_id
self.db.commit()
self.db.refresh(alert)
logger.info(f"Acknowledged alert {alert_id} by user {user_id}")
return alert
def update_alert(
self,
*,
alert_id: int,
title: Optional[str] = None,
message: Optional[str] = None,
alert_date: Optional[date] = None,
is_active: Optional[bool] = None,
) -> FileAlert:
alert = self.db.query(FileAlert).filter(FileAlert.id == alert_id).first()
if not alert:
raise FileManagementError("Alert not found")
if title is not None:
alert.title = title
if message is not None:
alert.message = message
if alert_date is not None:
alert.alert_date = alert_date
if is_active is not None:
alert.is_active = bool(is_active)
self.db.commit()
self.db.refresh(alert)
logger.info(f"Updated alert {alert_id}")
return alert
def delete_alert(self, *, alert_id: int) -> None:
alert = self.db.query(FileAlert).filter(FileAlert.id == alert_id).first()
if not alert:
raise FileManagementError("Alert not found")
self.db.delete(alert)
self.db.commit()
logger.info(f"Deleted alert {alert_id}")
# Relationship management
def create_relationship(
self,
*,
source_file_no: str,
target_file_no: str,
relationship_type: str,
user_id: Optional[int] = None,
notes: Optional[str] = None,
) -> FileRelationship:
if source_file_no == target_file_no:
raise FileManagementError("Source and target file cannot be the same")
source = self.db.query(File).filter(File.file_no == source_file_no).first()
target = self.db.query(File).filter(File.file_no == target_file_no).first()
if not source:
raise FileManagementError(f"File {source_file_no} not found")
if not target:
raise FileManagementError(f"File {target_file_no} not found")
user_name: Optional[str] = None
if user_id is not None:
user = self.db.query(User).filter(User.id == user_id).first()
user_name = user.username if user else f"user_{user_id}"
# Prevent duplicate exact relationship
existing = self.db.query(FileRelationship).filter(
FileRelationship.source_file_no == source_file_no,
FileRelationship.target_file_no == target_file_no,
FileRelationship.relationship_type == relationship_type,
).first()
if existing:
return existing
rel = FileRelationship(
source_file_no=source_file_no,
target_file_no=target_file_no,
relationship_type=relationship_type,
notes=notes,
created_by_user_id=user_id,
created_by_name=user_name,
)
self.db.add(rel)
self.db.commit()
self.db.refresh(rel)
logger.info(
f"Created relationship {relationship_type}: {source_file_no} -> {target_file_no}"
)
return rel
def get_relationships(self, *, file_no: str) -> List[Dict[str, Any]]:
"""Return relationships where the given file is source or target."""
rels = self.db.query(FileRelationship).filter(
(FileRelationship.source_file_no == file_no) | (FileRelationship.target_file_no == file_no)
).order_by(FileRelationship.id.desc()).all()
results: List[Dict[str, Any]] = []
for r in rels:
direction = "outbound" if r.source_file_no == file_no else "inbound"
other_file_no = r.target_file_no if direction == "outbound" else r.source_file_no
results.append(
{
"id": r.id,
"direction": direction,
"relationship_type": r.relationship_type,
"notes": r.notes,
"source_file_no": r.source_file_no,
"target_file_no": r.target_file_no,
"other_file_no": other_file_no,
"created_by_name": r.created_by_name,
"created_at": getattr(r, "created_at", None),
}
)
return results
def delete_relationship(self, *, relationship_id: int) -> None:
rel = self.db.query(FileRelationship).filter(FileRelationship.id == relationship_id).first()
if not rel:
raise FileManagementError("Relationship not found")
self.db.delete(rel)
self.db.commit()
logger.info(f"Deleted relationship {relationship_id}")
# Private helper methods