246 lines
11 KiB
Python
246 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from collections import OrderedDict
|
|
from datetime import date, timedelta
|
|
from typing import Any
|
|
|
|
from sqlalchemy import or_, select
|
|
from sqlalchemy.orm import Session, selectinload
|
|
|
|
from app.modules.clients.models import Client
|
|
from app.modules.consultants.models import ClientConsultantLink, ConsultantProfile, ConsultantServiceRequest
|
|
from app.modules.documents.models import EngagementDocument, PermanentClientDocument
|
|
from app.modules.services.models import (
|
|
ClientServiceSubscription,
|
|
ClientServiceTaskInstance,
|
|
ServiceCatalogue,
|
|
ServiceTaskComment,
|
|
)
|
|
|
|
CONSULTANT_BOARD_COLUMNS = OrderedDict(
|
|
[
|
|
("assigned", "Assigned"),
|
|
("awaiting_documents", "Awaiting Documents"),
|
|
("in_progress", "In Progress"),
|
|
("submitted", "Submitted"),
|
|
("accepted", "Accepted"),
|
|
("closed", "Closed"),
|
|
]
|
|
)
|
|
|
|
|
|
def _allowed_client_ids(db: Session, *, consultant: ConsultantProfile, require_communications: bool = False) -> list[int]:
|
|
query = select(ClientConsultantLink.client_id).where(
|
|
ClientConsultantLink.tenant_id == consultant.tenant_id,
|
|
ClientConsultantLink.consultant_id == consultant.id,
|
|
ClientConsultantLink.is_active.is_(True),
|
|
)
|
|
if require_communications:
|
|
query = query.where(ClientConsultantLink.can_view_communications.is_(True))
|
|
return [int(x) for x in db.execute(query).scalars().all()]
|
|
|
|
|
|
def _board_key_for_status(status: str | None) -> str:
|
|
value = (status or "pending").strip().lower()
|
|
if value in {"blocked", "awaiting_documents", "document_pending", "clarification_required"}:
|
|
return "awaiting_documents"
|
|
if value in {"in_progress", "under_process", "processing", "started"}:
|
|
return "in_progress"
|
|
if value in {"pending_review", "ready_for_review", "submitted", "completed"}:
|
|
return "submitted"
|
|
if value in {"approved", "accepted"}:
|
|
return "accepted"
|
|
if value in {"closed", "locked", "cancelled", "inactive"}:
|
|
return "closed"
|
|
return "assigned"
|
|
|
|
|
|
def _matches_search(*values: Any, q: str = "") -> bool:
|
|
term = (q or "").strip().lower()
|
|
if not term:
|
|
return True
|
|
return any(term in str(v or "").lower() for v in values)
|
|
|
|
|
|
def get_consultant_work_board(db: Session, *, consultant: ConsultantProfile, q: str = "", status: str = "") -> dict:
|
|
"""Build consultant work board from consultant-visible firm task communications.
|
|
|
|
The board intentionally uses existing task/comment visibility rules only. A consultant sees a task here only when
|
|
the firm has linked the consultant to the client and has created a consultant-visible communication for that task.
|
|
"""
|
|
client_ids = _allowed_client_ids(db, consultant=consultant, require_communications=True)
|
|
columns = {key: {"label": label, "items": []} for key, label in CONSULTANT_BOARD_COLUMNS.items()}
|
|
latest_by_task: dict[int, dict] = {}
|
|
|
|
if client_ids:
|
|
rows = db.execute(
|
|
select(ServiceTaskComment, ClientServiceTaskInstance, ClientServiceSubscription, Client, ServiceCatalogue)
|
|
.join(ClientServiceTaskInstance, ClientServiceTaskInstance.id == ServiceTaskComment.task_instance_id)
|
|
.join(ClientServiceSubscription, ClientServiceSubscription.id == ServiceTaskComment.subscription_id)
|
|
.join(Client, Client.id == ClientServiceTaskInstance.client_id)
|
|
.join(ServiceCatalogue, ServiceCatalogue.id == ClientServiceTaskInstance.service_catalogue_id)
|
|
.where(
|
|
ServiceTaskComment.tenant_id == consultant.tenant_id,
|
|
ServiceTaskComment.visibility == "consultant",
|
|
ServiceTaskComment.is_deleted.is_(False),
|
|
ClientServiceTaskInstance.client_id.in_(client_ids),
|
|
ClientServiceTaskInstance.tenant_id == consultant.tenant_id,
|
|
ClientServiceTaskInstance.is_active.is_(True),
|
|
)
|
|
.order_by(ServiceTaskComment.created_at_utc.desc(), ServiceTaskComment.id.desc())
|
|
.limit(300)
|
|
).all()
|
|
consultant_user_id = int(getattr(consultant, "user_id", 0) or 0)
|
|
for comment, task, subscription, client, catalogue in rows:
|
|
if int(task.id) in latest_by_task:
|
|
continue
|
|
if not _matches_search(client.client_name, getattr(client, "client_code", ""), catalogue.service_name, task.task_name, comment.message, q=q):
|
|
continue
|
|
key = _board_key_for_status(task.status)
|
|
if status and key != status:
|
|
continue
|
|
latest_by_task[int(task.id)] = {
|
|
"comment": comment,
|
|
"task": task,
|
|
"subscription": subscription,
|
|
"client": client,
|
|
"catalogue": catalogue,
|
|
"board_key": key,
|
|
"last_message_from_consultant": int(getattr(comment, "created_by_user_id", 0) or 0) == consultant_user_id,
|
|
"is_overdue": bool(getattr(task, "internal_target_date", None) and task.internal_target_date < date.today()),
|
|
}
|
|
|
|
for item in latest_by_task.values():
|
|
columns[item["board_key"]]["items"].append(item)
|
|
|
|
service_requests = db.execute(
|
|
select(ConsultantServiceRequest)
|
|
.options(
|
|
selectinload(ConsultantServiceRequest.managed_client),
|
|
selectinload(ConsultantServiceRequest.firm_client),
|
|
selectinload(ConsultantServiceRequest.service_catalogue),
|
|
)
|
|
.where(
|
|
ConsultantServiceRequest.tenant_id == consultant.tenant_id,
|
|
ConsultantServiceRequest.consultant_id == consultant.id,
|
|
ConsultantServiceRequest.is_active.is_(True),
|
|
)
|
|
.order_by(ConsultantServiceRequest.created_at_utc.desc(), ConsultantServiceRequest.id.desc())
|
|
.limit(50)
|
|
).scalars().all()
|
|
|
|
return {
|
|
"columns": columns,
|
|
"column_options": list(CONSULTANT_BOARD_COLUMNS.items()),
|
|
"total_tasks": len(latest_by_task),
|
|
"service_requests": service_requests,
|
|
}
|
|
|
|
|
|
def get_consultant_assignment_detail(db: Session, *, consultant: ConsultantProfile, task_id: int) -> dict | None:
|
|
client_ids = _allowed_client_ids(db, consultant=consultant, require_communications=True)
|
|
if not client_ids:
|
|
return None
|
|
row = db.execute(
|
|
select(ClientServiceTaskInstance, ClientServiceSubscription, Client, ServiceCatalogue)
|
|
.join(ClientServiceSubscription, ClientServiceSubscription.id == ClientServiceTaskInstance.subscription_id)
|
|
.join(Client, Client.id == ClientServiceTaskInstance.client_id)
|
|
.join(ServiceCatalogue, ServiceCatalogue.id == ClientServiceTaskInstance.service_catalogue_id)
|
|
.where(
|
|
ClientServiceTaskInstance.id == task_id,
|
|
ClientServiceTaskInstance.tenant_id == consultant.tenant_id,
|
|
ClientServiceTaskInstance.client_id.in_(client_ids),
|
|
ClientServiceTaskInstance.is_active.is_(True),
|
|
)
|
|
).first()
|
|
if not row:
|
|
return None
|
|
task, subscription, client, catalogue = row
|
|
timeline = db.execute(
|
|
select(ServiceTaskComment)
|
|
.options(selectinload(ServiceTaskComment.created_by))
|
|
.where(
|
|
ServiceTaskComment.tenant_id == consultant.tenant_id,
|
|
ServiceTaskComment.task_instance_id == task.id,
|
|
ServiceTaskComment.visibility == "consultant",
|
|
ServiceTaskComment.is_deleted.is_(False),
|
|
)
|
|
.order_by(ServiceTaskComment.created_at_utc.asc(), ServiceTaskComment.id.asc())
|
|
).scalars().all()
|
|
engagement_documents = db.execute(
|
|
select(EngagementDocument)
|
|
.options(selectinload(EngagementDocument.versions))
|
|
.where(
|
|
EngagementDocument.tenant_id == consultant.tenant_id,
|
|
EngagementDocument.client_id == client.id,
|
|
EngagementDocument.engagement_id == subscription.id,
|
|
EngagementDocument.is_deleted.is_(False),
|
|
)
|
|
.order_by(EngagementDocument.document_type.asc(), EngagementDocument.title.asc())
|
|
.limit(50)
|
|
).scalars().all()
|
|
permanent_documents = db.execute(
|
|
select(PermanentClientDocument)
|
|
.options(selectinload(PermanentClientDocument.versions))
|
|
.where(
|
|
PermanentClientDocument.tenant_id == consultant.tenant_id,
|
|
PermanentClientDocument.client_id == client.id,
|
|
PermanentClientDocument.is_deleted.is_(False),
|
|
)
|
|
.order_by(PermanentClientDocument.category.asc(), PermanentClientDocument.title.asc())
|
|
.limit(50)
|
|
).scalars().all()
|
|
return {
|
|
"task": task,
|
|
"subscription": subscription,
|
|
"client": client,
|
|
"catalogue": catalogue,
|
|
"timeline": timeline,
|
|
"engagement_documents": engagement_documents,
|
|
"permanent_documents": permanent_documents,
|
|
}
|
|
|
|
|
|
def get_consultant_document_centre(db: Session, *, consultant: ConsultantProfile, q: str = "") -> dict:
|
|
client_ids = _allowed_client_ids(db, consultant=consultant, require_communications=False)
|
|
if not client_ids:
|
|
return {"engagement_documents": [], "permanent_documents": [], "total": 0}
|
|
|
|
engagement_query = (
|
|
select(EngagementDocument)
|
|
.options(selectinload(EngagementDocument.client), selectinload(EngagementDocument.engagement), selectinload(EngagementDocument.versions))
|
|
.where(
|
|
EngagementDocument.tenant_id == consultant.tenant_id,
|
|
EngagementDocument.client_id.in_(client_ids),
|
|
EngagementDocument.is_deleted.is_(False),
|
|
)
|
|
)
|
|
permanent_query = (
|
|
select(PermanentClientDocument)
|
|
.options(selectinload(PermanentClientDocument.client), selectinload(PermanentClientDocument.versions))
|
|
.where(
|
|
PermanentClientDocument.tenant_id == consultant.tenant_id,
|
|
PermanentClientDocument.client_id.in_(client_ids),
|
|
PermanentClientDocument.is_deleted.is_(False),
|
|
)
|
|
)
|
|
if (q or "").strip():
|
|
term = f"%{q.strip()}%"
|
|
engagement_query = engagement_query.where(
|
|
or_(EngagementDocument.title.ilike(term), EngagementDocument.document_type.ilike(term), EngagementDocument.document_code.ilike(term))
|
|
)
|
|
permanent_query = permanent_query.where(
|
|
or_(PermanentClientDocument.title.ilike(term), PermanentClientDocument.category.ilike(term), PermanentClientDocument.document_code.ilike(term))
|
|
)
|
|
engagement_documents = db.execute(
|
|
engagement_query.order_by(EngagementDocument.created_at_utc.desc(), EngagementDocument.id.desc()).limit(200)
|
|
).scalars().all()
|
|
permanent_documents = db.execute(
|
|
permanent_query.order_by(PermanentClientDocument.created_at_utc.desc(), PermanentClientDocument.id.desc()).limit(200)
|
|
).scalars().all()
|
|
return {
|
|
"engagement_documents": engagement_documents,
|
|
"permanent_documents": permanent_documents,
|
|
"total": len(engagement_documents) + len(permanent_documents),
|
|
}
|