From 798a2532c10065d7bab93742b9fc18f9461ed8b7 Mon Sep 17 00:00:00 2001 From: A R R R Associates Date: Fri, 21 Aug 2026 14:28:32 +0530 Subject: [PATCH] Improve staff My Work board filters business display and pagination --- app/modules/employees/service.py | 246 +++++++++++++----- .../templates/employees/self_work.html | 212 ++++++--------- app/modules/employees/ui.py | 29 ++- 3 files changed, 282 insertions(+), 205 deletions(-) diff --git a/app/modules/employees/service.py b/app/modules/employees/service.py index 29f42bb..78651c5 100644 --- a/app/modules/employees/service.py +++ b/app/modules/employees/service.py @@ -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, diff --git a/app/modules/employees/templates/employees/self_work.html b/app/modules/employees/templates/employees/self_work.html index 3f7743a..947f138 100644 --- a/app/modules/employees/templates/employees/self_work.html +++ b/app/modules/employees/templates/employees/self_work.html @@ -31,9 +31,9 @@
-
- - + - + + + + + +
+
+
FY {{ financial_year }} · Default ordering remains earliest due date first.
+
+ Clear + +
{% for column in work_payload.columns %}
-
+

{{ column.label }}

{% if column.code == 'pending' %}

Assigned engagements where work has not started.

{% endif %} @@ -56,156 +90,68 @@ {% if column.code == 'blocked' %}

Engagements waiting for documents, clarification, review or another dependency.

{% endif %} {% if column.code == 'completed' %}

All tasks assigned to you in these engagements are complete.

{% endif %}
- {{ column.total_cards }} +
+ {{ column.total_cards }} + {% if column.total_cards %}
Showing {{ column.pagination.start }}-{{ column.pagination.end }} of {{ column.pagination.total }}
{% endif %} +
- - - - - - - - - - - - +
Client / ScopeServiceFY / DueProgressNext / PendingStatusAction
+ + + {% for card in column.cards %} - + + + - - - - - - - - - - + + - {% else %} - - - - {% endfor %} + {% else %}{% endfor %}
Business / Client / ScopeService / PeriodFY / DueProgressNext / PendingStatusAction
-
{{ card.client_code or 'Client' }}
+
{{ card.client_code or card.scope_label or 'Client' }}
{{ card.client_name }}
- {% if card.scope_secondary %} -
{{ card.scope_secondary }}
- {% endif %} - {% if card.scope_context %} -
{{ card.scope_context }}
- {% endif %} + {% if card.legal_client_name %}
Client: {{ card.legal_client_name }}
{% endif %} + {% if card.scope_secondary %}
{{ card.scope_secondary }}
{% endif %} + {% if card.scope_context and not card.legal_client_name %}
{{ card.scope_context }}
{% endif %}
{{ card.service_name }}
{% if card.period_label %}
Period: {{ card.period_label }}
{% endif %}
FY {{ card.financial_year }}
Due {{ card.due_date or '-' }}
{{ card.progress_percent }}%{{ card.completed_count }}/{{ card.task_count }} tasks
Weighted progress
-
{{ card.service_name }}
- {% if card.period_label %} -
{{ card.period_label }}
- {% endif %} -
-
FY {{ card.financial_year }}
-
Due {{ card.due_date or '-' }}
-
-
-
- {{ card.progress_percent }}% - {{ card.completed_count }}/{{ card.task_count }} tasks -
-
-
-
-
Weighted progress
-
-
- {% if column.code == 'blocked' %} -
-
{{ card.blocked_reason }}
- {% if card.blocked_notes %}
{{ card.blocked_notes }}
{% endif %} - {% if card.follow_up_date %}
Follow-up: {{ card.follow_up_date }}
{% endif %} - {% if card.blocked_task_name %}
Task: {{ card.blocked_task_name }}
{% endif %} -
- {% elif card.next_task_name and column.code != 'completed' %} -
{{ card.next_task_name }}
- {% else %} - — - {% endif %} - {% if card.latest_comment %} -
Latest: {{ card.latest_comment.message }}
- {% endif %} -
-
- {% if card.is_overdue %} - Overdue - {% elif card.is_due_today %} - Due today - {% endif %} - - {% if column.code == 'pending' %} - Pending - {% elif column.code == 'in_progress' %} - In Progress - {% elif column.code == 'blocked' %} - Blocked - {% else %} - Completed - {% endif %} -
-
-
- {% if column.code == 'pending' %} -
- - -
- {% elif column.code == 'in_progress' %} - Continue - {% elif column.code == 'blocked' %} - Open / Follow Up - {% if card.manual_blocker %} -
- - -
- {% endif %} - {% else %} - View - {% endif %} -
+ {% if column.code == 'blocked' %}
{{ card.blocked_reason }}
{% if card.blocked_notes %}
{{ card.blocked_notes }}
{% endif %}{% if card.follow_up_date %}
Follow-up: {{ card.follow_up_date }}
{% endif %}{% if card.blocked_task_name %}
Task: {{ card.blocked_task_name }}
{% endif %}
+ {% elif card.next_task_name and column.code != 'completed' %}
{{ card.next_task_name }}
{% else %}—{% endif %} + {% if card.latest_comment %}
Latest: {{ card.latest_comment.message }}
{% endif %}
+ {% if card.is_overdue %}Overdue{% elif card.is_due_today %}Due today{% endif %} + {% if column.code == 'pending' %}Pending{% elif column.code == 'in_progress' %}In Progress{% elif column.code == 'blocked' %}Blocked{% else %}Completed{% endif %} +
+ {% if column.code == 'pending' %}
+ {% elif column.code == 'in_progress' %}Continue + {% elif column.code == 'blocked' %}Open / Follow Up{% if card.manual_blocker %}
{% endif %} + {% else %}View{% endif %} +
- No engagements in this section. -
No engagements in this section.
- {% if column.code == 'completed' and work_payload.completed_pagination.pages > 1 %} + {% if column.pagination.pages > 1 %}
-
- Showing {{ work_payload.completed_pagination.start }}-{{ work_payload.completed_pagination.end }} of {{ work_payload.completed_pagination.total }} completed engagements -
+
Showing {{ column.pagination.start }}-{{ column.pagination.end }} of {{ column.pagination.total }} {{ column.label|lower }} engagements
- {% if work_payload.completed_pagination.has_previous %} + {% if column.pagination.has_previous %}
- - - + + -
- {% endif %} - Page {{ work_payload.completed_pagination.page }} of {{ work_payload.completed_pagination.pages }} - {% if work_payload.completed_pagination.has_next %} + {% endif %} + Page {{ column.pagination.page }} of {{ column.pagination.pages }} + {% if column.pagination.has_next %}
- - - + + -
- {% endif %} + {% endif %}
{% endif %} diff --git a/app/modules/employees/ui.py b/app/modules/employees/ui.py index 007f794..a94d563 100644 --- a/app/modules/employees/ui.py +++ b/app/modules/employees/ui.py @@ -2027,7 +2027,20 @@ def employee_my_work_task_status( @portal_router.get("/work") -def employee_my_work(request: Request, q: str = "", status: str = "open", completed_page: int = 1): +def employee_my_work( + request: Request, + q: str = "", + status: str = "open", + period: str = "", + service: str = "", + due: 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, +): db = CommonSessionLocal() try: current_user = get_current_user(request, db=db) @@ -2048,8 +2061,15 @@ def employee_my_work(request: Request, q: str = "", status: str = "open", comple q=q, status=status, financial_year=financial_year, + period=period, + service=service, + due_bucket=due, + sort=sort, + page_size=page_size, + in_progress_page=in_progress_page, + pending_page=pending_page, + blocked_page=blocked_page, completed_page=completed_page, - completed_page_size=25, ) return _render( request, @@ -2061,6 +2081,11 @@ def employee_my_work(request: Request, q: str = "", status: str = "open", comple work_payload=work_payload, q=q, status=status, + period=period, + service=service, + due=due, + sort=sort, + page_size=work_payload.get("filters", {}).get("page_size", 10), errors=[], financial_year=financial_year, )