Improve staff My Work board filters business display and pagination

This commit is contained in:
A R R R Associates
2026-08-21 14:28:32 +05:30
parent 208808da6a
commit 798a2532c1
3 changed files with 282 additions and 205 deletions
+176 -70
View File
@@ -3112,16 +3112,23 @@ def list_employee_work_kanban(
q: str = "",
status: str = "open",
financial_year: str | None = None,
period: str = "",
service: str = "",
due_bucket: str = "",
sort: str = "due_asc",
page_size: int = 10,
in_progress_page: int = 1,
pending_page: int = 1,
blocked_page: int = 1,
completed_page: int = 1,
completed_page_size: int = 25,
) -> dict[str, Any]:
"""Return exactly one employee-board card per assigned engagement.
The task engine remains unchanged. The board derives an engagement state from
the staff member's assigned tasks using this precedence:
This is still the existing task/engagement workflow. The board only adds
presentation filters and independent pagination. Engagement state remains:
completed -> all assigned tasks are closed
blocked -> at least one assigned task is blocked
blocked -> at least one assigned task is blocked / workflow blocker exists
in_progress -> at least one assigned task is started/completed
pending -> no assigned task has started
"""
@@ -3131,30 +3138,36 @@ def list_employee_work_kanban(
if status_filter not in valid_filters:
status_filter = "open"
q_filter = (q or "").strip().lower()
period_filter = (period or "").strip()
service_filter = (service or "").strip()
due_filter = (due_bucket or "").strip().lower()
valid_due_filters = {"", "overdue", "today", "next_7", "next_15", "this_month", "no_due"}
if due_filter not in valid_due_filters:
due_filter = ""
sort_filter = (sort or "due_asc").strip().lower()
valid_sorts = {"due_asc", "due_desc", "business_asc", "service_asc"}
if sort_filter not in valid_sorts:
sort_filter = "due_asc"
try:
page_size = max(1, min(int(page_size or 10), 50))
except (TypeError, ValueError):
page_size = 10
if page_size not in {10, 25, 50}:
page_size = 10
stmt = _employee_work_task_query(
db,
scope,
assigned_only=True,
financial_year=financial_year,
)
if q.strip():
like = f"%{q.strip()}%"
stmt = stmt.where(
or_(
ClientServiceTaskInstance.task_name.ilike(like),
ClientServiceTaskInstance.description.ilike(like),
ClientServiceTaskInstance.client.has(
or_(Client.client_name.ilike(like), Client.trade_name.ilike(like), Client.client_code.ilike(like))
),
ClientServiceTaskInstance.catalogue.has(
or_(
ServiceCatalogue.service_name.ilike(like),
ServiceCatalogue.service_code.ilike(like),
)
),
)
)
# Search is deliberately applied after engagement/scope resolution below.
# That retains task/client/service search and also allows registration,
# business-unit, branch, trade-name and GSTIN searches.
tasks = db.execute(
stmt.order_by(
ClientServiceTaskInstance.subscription_id.asc(),
@@ -3208,16 +3221,29 @@ def list_employee_work_kanban(
"context": "",
"scope_label": "Client",
"client_code": getattr(client, "client_code", None) or "",
"client_name": getattr(client, "client_name", None) or "",
"client_trade_name": getattr(client, "trade_name", None) or "",
},
)
primary_name = (scope_display.get("primary") or "Unlinked Client").strip()
legal_client_name = (scope_display.get("client_name") or getattr(client, "client_name", None) or "").strip()
# Business/trade/scope name is primary. Show legal client name only
# when it is genuinely different, ignoring case and whitespace.
norm_primary = " ".join(primary_name.casefold().split())
norm_client = " ".join(legal_client_name.casefold().split())
client_context = (scope_display.get("context") or "").strip()
if legal_client_name and norm_client == norm_primary:
client_context = ""
engagement_lookup[engagement_id] = {
"engagement_id": engagement_id,
"subscription": subscription,
"label": _subscription_label(subscription, task),
"client_name": scope_display["primary"],
"client_name": primary_name,
"legal_client_name": legal_client_name if norm_client != norm_primary else "",
"client_code": scope_display.get("client_code", "") or getattr(client, "client_code", None) or "",
"scope_secondary": scope_display.get("secondary", ""),
"scope_context": scope_display.get("context", ""),
"scope_context": client_context,
"scope_label": scope_display.get("scope_label", "Client"),
"service_name": (
getattr(getattr(subscription, "catalogue", None), "service_name", None)
@@ -3229,6 +3255,11 @@ def list_employee_work_kanban(
or getattr(task, "financial_year", None)
or "-"
),
"period_label": (
(getattr(subscription, "period_label", None) if subscription else None)
or getattr(task, "period_label", None)
or ""
),
"due_date": (
getattr(subscription, "current_due_date", None)
if subscription
@@ -3260,11 +3291,7 @@ def list_employee_work_kanban(
if not card["blocked_reason"]:
card["blocked_reason"] = (
(getattr(task, "remarks", None) or "").strip()
or (
getattr(task.latest_comment, "message", None)
if task.latest_comment
else None
)
or (getattr(task.latest_comment, "message", None) if task.latest_comment else None)
or "Work is awaiting a dependency or clarification."
)
card["blocked_task_name"] = task.task_name
@@ -3282,6 +3309,10 @@ def list_employee_work_kanban(
if not is_closed and not card["next_task_name"]:
card["next_task_name"] = task.task_name
# Options are derived from the employee's assigned engagements in the active FY.
period_options = sorted({str(card.get("period_label") or "").strip() for card in engagement_lookup.values() if str(card.get("period_label") or "").strip()})
service_options = sorted({str(card.get("service_name") or "").strip() for card in engagement_lookup.values() if str(card.get("service_name") or "").strip()}, key=str.casefold)
summary = {
"total": 0,
"open": 0,
@@ -3293,6 +3324,26 @@ def list_employee_work_kanban(
"due_today": 0,
}
def _matches_due(card: dict[str, Any]) -> bool:
due_date = card.get("due_date")
if not due_filter:
return True
if due_filter == "no_due":
return due_date is None
if due_date is None:
return False
if due_filter == "overdue":
return due_date < today
if due_filter == "today":
return due_date == today
if due_filter == "next_7":
return today <= due_date <= (today + timedelta(days=7))
if due_filter == "next_15":
return today <= due_date <= (today + timedelta(days=15))
if due_filter == "this_month":
return due_date.year == today.year and due_date.month == today.month
return True
for card in engagement_lookup.values():
total = card["task_count"]
completed = card["completed_count"]
@@ -3330,14 +3381,43 @@ def list_employee_work_kanban(
card["is_overdue"] = bool(card["overdue_count"])
card["is_due_today"] = bool(card["due_today_count"])
include = (
include_status = (
status_filter == "open"
or (status_filter == "closed" and card_status == "completed")
or status_filter == card_status
)
if not include:
if not include_status:
continue
if period_filter and str(card.get("period_label") or "") != period_filter:
continue
if service_filter and str(card.get("service_name") or "") != service_filter:
continue
if not _matches_due(card):
continue
if q_filter:
task_search_text = " ".join(
f"{getattr(task, 'task_name', '') or ''} {getattr(task, 'description', '') or ''}"
for task in card.get("tasks", [])
)
haystack = " ".join(
str(value or "")
for value in (
card.get("client_name"),
card.get("legal_client_name"),
card.get("client_code"),
card.get("scope_secondary"),
card.get("scope_context"),
card.get("service_name"),
card.get("period_label"),
card.get("financial_year"),
task_search_text,
)
).lower()
if q_filter not in haystack:
continue
column_lookup[card_status]["cards"].append(card)
summary["total"] += 1
if card_status == "completed":
@@ -3350,47 +3430,61 @@ def list_employee_work_kanban(
if card["is_due_today"]:
summary["due_today"] += 1
for column in columns:
column["cards"].sort(
key=lambda card: (
card["due_date"] is None,
card["due_date"] or date.max,
card["client_name"].lower(),
card["service_name"].lower(),
def _sort_cards(cards: list[dict[str, Any]]) -> None:
if sort_filter == "due_desc":
cards.sort(
key=lambda card: (
card["due_date"] is not None,
card["due_date"] or date.min,
card["client_name"].casefold(),
card["service_name"].casefold(),
),
reverse=True,
)
)
column["total_cards"] = len(column["cards"])
elif sort_filter == "business_asc":
cards.sort(key=lambda card: (card["client_name"].casefold(), card["due_date"] is None, card["due_date"] or date.max, card["service_name"].casefold()))
elif sort_filter == "service_asc":
cards.sort(key=lambda card: (card["service_name"].casefold(), card["due_date"] is None, card["due_date"] or date.max, card["client_name"].casefold()))
else:
# Existing behaviour: earliest due date first, undated work last.
cards.sort(key=lambda card: (card["due_date"] is None, card["due_date"] or date.max, card["client_name"].casefold(), card["service_name"].casefold()))
try:
completed_page = max(int(completed_page or 1), 1)
except (TypeError, ValueError):
completed_page = 1
try:
completed_page_size = max(1, min(int(completed_page_size or 25), 100))
except (TypeError, ValueError):
completed_page_size = 25
completed_column = column_lookup["completed"]
completed_total = int(completed_column["total_cards"])
completed_pages = max(1, (completed_total + completed_page_size - 1) // completed_page_size)
if completed_page > completed_pages:
completed_page = completed_pages
completed_start = (completed_page - 1) * completed_page_size
completed_end = completed_start + completed_page_size
completed_column["cards"] = completed_column["cards"][completed_start:completed_end]
completed_pagination = {
"page": completed_page,
"page_size": completed_page_size,
"total": completed_total,
"pages": completed_pages,
"has_previous": completed_page > 1,
"has_next": completed_page < completed_pages,
"previous_page": completed_page - 1 if completed_page > 1 else None,
"next_page": completed_page + 1 if completed_page < completed_pages else None,
"start": completed_start + 1 if completed_total else 0,
"end": min(completed_end, completed_total),
page_requests = {
"in_progress": in_progress_page,
"pending": pending_page,
"blocked": blocked_page,
"completed": completed_page,
}
paginations: dict[str, dict[str, Any]] = {}
for column in columns:
_sort_cards(column["cards"])
total_cards = len(column["cards"])
column["total_cards"] = total_cards
try:
page = max(int(page_requests.get(column["code"], 1) or 1), 1)
except (TypeError, ValueError):
page = 1
pages = max(1, (total_cards + page_size - 1) // page_size)
if page > pages:
page = pages
start_index = (page - 1) * page_size
end_index = start_index + page_size
column["cards"] = column["cards"][start_index:end_index]
pagination = {
"page": page,
"page_size": page_size,
"total": total_cards,
"pages": pages,
"has_previous": page > 1,
"has_next": page < pages,
"previous_page": page - 1 if page > 1 else None,
"next_page": page + 1 if page < pages else None,
"start": start_index + 1 if total_cards else 0,
"end": min(end_index, total_cards),
}
column["pagination"] = pagination
paginations[column["code"]] = pagination
return {
"summary": summary,
@@ -3398,10 +3492,22 @@ def list_employee_work_kanban(
"q": q,
"status": status_filter,
"today": today,
"completed_pagination": completed_pagination,
"filters": {
"period": period_filter,
"service": service_filter,
"due": due_filter,
"sort": sort_filter,
"page_size": page_size,
},
"filter_options": {
"periods": period_options,
"services": service_options,
},
"paginations": paginations,
# Backward compatible for any template/consumer still using the old key.
"completed_pagination": paginations["completed"],
}
def start_employee_engagement_workflow(
db: Session,
scope: EmployeeScope,