-
Partner Review Board
-
Engagement-level review queue. Task-level review, rework and history remain available inside each engagement.
+
+
+
+
Partner Review Board
+
Engagement-level review queue with business identity, engagement period, statutory due date and independent pagination for each section.
+
+
Active FY context: {{ payload.active_financial_year or 'Not set' }}
-
- {% set metrics = namespace(pending_review=[], clarification_required=[], rework_sent=[], approved=[], completed=[]) %}
+ {% set metrics = namespace(pending_review=[], clarification_required=[], rework_sent=[], approved=[], completed=[], overdue=[], due_soon=[]) %}
{% for column in payload.columns %}
{% for task in column.tasks %}
{% if task.subscription_id %}
- {% if column.code == 'pending_review' and task.subscription_id not in metrics.pending_review %}
- {% set _ = metrics.pending_review.append(task.subscription_id) %}
- {% elif column.code == 'clarification_required' and task.subscription_id not in metrics.clarification_required %}
- {% set _ = metrics.clarification_required.append(task.subscription_id) %}
- {% elif column.code == 'rework_sent' and task.subscription_id not in metrics.rework_sent %}
- {% set _ = metrics.rework_sent.append(task.subscription_id) %}
- {% elif column.code == 'approved' and task.subscription_id not in metrics.approved %}
- {% set _ = metrics.approved.append(task.subscription_id) %}
- {% elif column.code == 'completed' and task.subscription_id not in metrics.completed %}
- {% set _ = metrics.completed.append(task.subscription_id) %}
- {% endif %}
+ {% if column.code == 'pending_review' and task.subscription_id not in metrics.pending_review %}{% set _ = metrics.pending_review.append(task.subscription_id) %}
+ {% elif column.code == 'clarification_required' and task.subscription_id not in metrics.clarification_required %}{% set _ = metrics.clarification_required.append(task.subscription_id) %}
+ {% elif column.code == 'rework_sent' and task.subscription_id not in metrics.rework_sent %}{% set _ = metrics.rework_sent.append(task.subscription_id) %}
+ {% elif column.code == 'approved' and task.subscription_id not in metrics.approved %}{% set _ = metrics.approved.append(task.subscription_id) %}
+ {% elif column.code == 'completed' and task.subscription_id not in metrics.completed %}{% set _ = metrics.completed.append(task.subscription_id) %}{% endif %}
+ {% if task.due_status_code == 'overdue' and task.subscription_id not in metrics.overdue %}{% set _ = metrics.overdue.append(task.subscription_id) %}{% endif %}
+ {% if task.due_status_code in ['today','soon'] and task.subscription_id not in metrics.due_soon %}{% set _ = metrics.due_soon.append(task.subscription_id) %}{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
-
-
-
Pending Review
-
{{ metrics.pending_review|length }}
-
Engagements with completed work awaiting review
-
-
-
Clarification Required
-
{{ metrics.clarification_required|length }}
-
Engagements with blocked tasks
-
-
-
Rework Sent
-
{{ metrics.rework_sent|length }}
-
Engagements containing rework items
-
-
-
In Progress
-
{{ metrics.approved|length }}
-
Engagements currently moving with the team
-
-
-
Completed
-
{{ metrics.completed|length }}
-
Engagements containing closed tasks
-
+
+
Pending Review
{{ metrics.pending_review|length }}
Awaiting partner review
+
Clarification
{{ metrics.clarification_required|length }}
Blocked for clarification
+
Rework
{{ metrics.rework_sent|length }}
Rework outstanding
+
In Progress
{{ metrics.approved|length }}
Moving with team
+
Completed
{{ metrics.completed|length }}
Completed engagements
+
Overdue
{{ metrics.overdue|length }}
Past engagement due date
+
Due ≤ 7 Days
{{ metrics.due_soon|length }}
Due today / next 7 days
- {# Each engagement appears in exactly one panel: review first, then in progress, then completed. #}
+ {# Exactly three board sections. Existing task-level states are preserved inside Review Required. #}
{% set groups = namespace(review=[], progress=[], completed=[]) %}
- {% for column in payload.columns %}
- {% if column.code in ['pending_review', 'clarification_required', 'rework_sent'] %}
- {% for task in column.tasks %}
- {% if task.subscription_id and task.subscription_id not in groups.review %}
- {% set _ = groups.review.append(task.subscription_id) %}
- {% endif %}
- {% endfor %}
- {% endif %}
- {% endfor %}
-
- {% for column in payload.columns %}
- {% if column.code == 'approved' %}
- {% for task in column.tasks %}
- {% if task.subscription_id and task.subscription_id not in groups.review and task.subscription_id not in groups.progress %}
- {% set _ = groups.progress.append(task.subscription_id) %}
- {% endif %}
- {% endfor %}
- {% endif %}
- {% endfor %}
-
- {% for column in payload.columns %}
- {% if column.code == 'completed' %}
- {% for task in column.tasks %}
- {% if task.subscription_id and task.subscription_id not in groups.review and task.subscription_id not in groups.progress and task.subscription_id not in groups.completed %}
- {% set _ = groups.completed.append(task.subscription_id) %}
- {% endif %}
- {% endfor %}
- {% endif %}
- {% endfor %}
+ {% for column in payload.columns %}{% if column.code in ['pending_review', 'clarification_required', 'rework_sent'] %}{% for task in column.tasks %}{% if task.subscription_id and task.subscription_id not in groups.review %}{% set _ = groups.review.append(task.subscription_id) %}{% endif %}{% endfor %}{% endif %}{% endfor %}
+ {% for column in payload.columns %}{% if column.code == 'approved' %}{% for task in column.tasks %}{% if task.subscription_id and task.subscription_id not in groups.review and task.subscription_id not in groups.progress %}{% set _ = groups.progress.append(task.subscription_id) %}{% endif %}{% endfor %}{% endif %}{% endfor %}
+ {% for column in payload.columns %}{% if column.code == 'completed' %}{% for task in column.tasks %}{% if task.subscription_id and task.subscription_id not in groups.review and task.subscription_id not in groups.progress and task.subscription_id not in groups.completed %}{% set _ = groups.completed.append(task.subscription_id) %}{% endif %}{% endfor %}{% endif %}{% endfor %}
{% set review_page = request.query_params.get('review_page', '1')|int %}
{% set progress_page = request.query_params.get('progress_page', '1')|int %}
{% set completed_page = request.query_params.get('completed_page', '1')|int %}
- {% set panel_page_size = 25 %}
- {{ panel(
- 'Review Required',
- 'Pending review, clarification-required and rework engagements are shown first.',
- groups.review,
- payload,
- 'review',
- 'review_page',
- review_page,
- panel_page_size,
- q or ''
- ) }}
+ {{ panel('Review Required', 'Pending review, clarification-required and rework engagements are shown first.', groups.review, payload, 'review', 'review_page', review_page, page_size, filter_query) }}
+ {{ panel('In Progress', 'Engagements currently moving with the team and not presently waiting in the review queue.', groups.progress, payload, 'progress', 'progress_page', progress_page, page_size, filter_query) }}
+ {{ panel('Completed', 'Completed engagement rows remain available separately for review history and reference.', groups.completed, payload, 'completed', 'completed_page', completed_page, page_size, filter_query) }}
- {{ panel(
- 'In Progress',
- 'Engagements currently moving with the team and not presently waiting in the review queue.',
- groups.progress,
- payload,
- 'progress',
- 'progress_page',
- progress_page,
- panel_page_size,
- q or ''
- ) }}
-
- {{ panel(
- 'Completed',
- 'Completed engagement rows remain available separately for review history and reference.',
- groups.completed,
- payload,
- 'completed',
- 'completed_page',
- completed_page,
- panel_page_size,
- q or ''
- ) }}
-
-
- Review remains task-level inside the engagement. This board only groups the existing task records into one engagement row for easier partner navigation.
-
+
Review remains task-level inside the engagement. This board only groups the existing task records into one engagement row for easier partner navigation.
{% endblock %}
diff --git a/app/modules/partners/ui.py b/app/modules/partners/ui.py
index 1404e34..50e9242 100644
--- a/app/modules/partners/ui.py
+++ b/app/modules/partners/ui.py
@@ -3,6 +3,7 @@ from __future__ import annotations
from collections import defaultdict
from datetime import date, datetime, timezone
from typing import Any
+from urllib.parse import urlencode
from fastapi import APIRouter, Form, Request
from fastapi.responses import RedirectResponse
@@ -15,7 +16,8 @@ from app.core.security.session_auth import get_current_user
from app.core.templating import templates
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.documents.models import EngagementDocument
-from app.modules.clients.models import Client
+from app.modules.clients.models import Client, ClientBranch, ClientBusinessUnit
+from app.modules.registrations.models import ClientRegistration, RegistrationType
from app.modules.services.execution import CLOSED_TASK_STATUSES, TASK_PRIORITIES, TASK_STATUSES
from app.modules.services.models import (
ClientServiceSubscription,
@@ -189,17 +191,237 @@ def _task_bucket(task: ClientServiceTaskInstance, current_user) -> str:
return "approved"
-def build_partner_payload(db: Session, request: Request, current_user, *, q: str = "") -> dict[str, Any]:
+def _normalise_name(value: str | None) -> str:
+ return " ".join((value or "").split()).casefold()
+
+
+def _partner_scope_display_map(
+ db: Session,
+ *,
+ tenant_id: int,
+ subscriptions: list[ClientServiceSubscription],
+) -> dict[int, dict[str, str]]:
+ """Resolve business/trade identity for the Partner Review Board.
+
+ Business/trade name is always primary. The legal client name is shown only
+ when it is genuinely different, preserving the engagement's existing scope.
+ """
+ subscriptions = [row for row in subscriptions if row is not None]
+ if not subscriptions:
+ return {}
+
+ business_ids = {int(row.business_unit_id) for row in subscriptions if getattr(row, "business_unit_id", None)}
+ branch_ids = {int(row.client_branch_id) for row in subscriptions if getattr(row, "client_branch_id", None)}
+ registration_ids = {int(row.registration_id) for row in subscriptions if getattr(row, "registration_id", None)}
+
+ businesses: dict[int, ClientBusinessUnit] = {}
+ if business_ids:
+ businesses = {
+ row.id: row for row in db.execute(
+ select(ClientBusinessUnit).where(
+ ClientBusinessUnit.tenant_id == tenant_id,
+ ClientBusinessUnit.id.in_(business_ids),
+ )
+ ).scalars().all()
+ }
+
+ branches: dict[int, ClientBranch] = {}
+ if branch_ids:
+ branches = {
+ row.id: row for row in db.execute(
+ select(ClientBranch).where(
+ ClientBranch.tenant_id == tenant_id,
+ ClientBranch.id.in_(branch_ids),
+ )
+ ).scalars().all()
+ }
+
+ registrations: dict[int, ClientRegistration] = {}
+ registration_types: dict[int, RegistrationType] = {}
+ if registration_ids:
+ for registration, registration_type in db.execute(
+ select(ClientRegistration, RegistrationType)
+ .join(RegistrationType, RegistrationType.id == ClientRegistration.registration_type_id)
+ .where(
+ ClientRegistration.tenant_id == tenant_id,
+ ClientRegistration.id.in_(registration_ids),
+ )
+ ).all():
+ registrations[registration.id] = registration
+ registration_types[registration.id] = registration_type
+
+ extra_business_ids = {
+ int(row.business_unit_id) for row in registrations.values()
+ if row.business_unit_id and row.business_unit_id not in businesses
+ }
+ if extra_business_ids:
+ businesses.update({
+ row.id: row for row in db.execute(
+ select(ClientBusinessUnit).where(
+ ClientBusinessUnit.tenant_id == tenant_id,
+ ClientBusinessUnit.id.in_(extra_business_ids),
+ )
+ ).scalars().all()
+ })
+
+ extra_branch_ids = {
+ int(row.client_branch_id) for row in registrations.values()
+ if row.client_branch_id and row.client_branch_id not in branches
+ }
+ if extra_branch_ids:
+ branches.update({
+ row.id: row for row in db.execute(
+ select(ClientBranch).where(
+ ClientBranch.tenant_id == tenant_id,
+ ClientBranch.id.in_(extra_branch_ids),
+ )
+ ).scalars().all()
+ })
+
+ result: dict[int, dict[str, str]] = {}
+ for subscription in subscriptions:
+ client = getattr(subscription, "client", None)
+ client_name = (getattr(client, "client_name", None) or "").strip()
+ client_trade_name = (getattr(client, "trade_name", None) or "").strip()
+ primary = client_trade_name or client_name or "Unlinked Client"
+ secondary_client = ""
+ scope_context = ""
+ scope_type = (getattr(subscription, "scope_type", None) or "client").strip().lower()
+
+ if scope_type == "registration":
+ registration = registrations.get(getattr(subscription, "registration_id", None))
+ registration_type = registration_types.get(getattr(subscription, "registration_id", None))
+ if registration:
+ business = businesses.get(getattr(registration, "business_unit_id", None) or getattr(subscription, "business_unit_id", None))
+ branch = branches.get(getattr(registration, "client_branch_id", None) or getattr(subscription, "client_branch_id", None))
+ business_trade = ((getattr(business, "trade_name", None) or "").strip() if business else "")
+ business_name = ((getattr(business, "business_name", None) or "").strip() if business else "")
+ branch_name = ((getattr(branch, "branch_name", None) or "").strip() if branch else "")
+ primary = (
+ (getattr(registration, "trade_name", None) or "").strip()
+ or business_trade or business_name or branch_name
+ or (getattr(registration, "legal_name", None) or "").strip()
+ or client_trade_name or client_name or "Unlinked Client"
+ )
+ reg_code = ((getattr(registration_type, "code", None) or "").strip() if registration_type else "")
+ reg_no = (getattr(registration, "registration_number", None) or "").strip()
+ scope_context = " · ".join(part for part in [reg_code, reg_no] if part)
+
+ elif scope_type == "business_unit":
+ business = businesses.get(getattr(subscription, "business_unit_id", None))
+ if business:
+ primary = ((getattr(business, "trade_name", None) or "").strip()
+ or (getattr(business, "business_name", None) or "").strip()
+ or primary)
+ scope_context = (getattr(business, "business_code", None) or "").strip()
+
+ elif scope_type == "client_branch":
+ branch = branches.get(getattr(subscription, "client_branch_id", None))
+ business = businesses.get(getattr(branch, "business_unit_id", None) if branch else getattr(subscription, "business_unit_id", None))
+ business_display = ""
+ if business:
+ business_display = ((getattr(business, "trade_name", None) or "").strip()
+ or (getattr(business, "business_name", None) or "").strip())
+ if branch:
+ primary = (getattr(branch, "branch_name", None) or "").strip() or business_display or primary
+ scope_context = (getattr(branch, "branch_code", None) or "").strip()
+ elif business_display:
+ primary = business_display
+
+ if client_name and _normalise_name(client_name) != _normalise_name(primary):
+ secondary_client = client_name
+
+ result[int(subscription.id)] = {
+ "primary": primary,
+ "client_name": client_name,
+ "secondary_client": secondary_client,
+ "scope_context": scope_context,
+ }
+ return result
+
+
+def _due_bucket_matches(due: date | None, bucket: str, today: date) -> bool:
+ bucket = (bucket or "").strip().lower()
+ if not bucket or bucket == "all":
+ return True
+ if bucket == "none":
+ return due is None
+ if due is None:
+ return False
+ delta = (due - today).days
+ if bucket == "overdue":
+ return delta < 0
+ if bucket == "today":
+ return delta == 0
+ if bucket == "next_7":
+ return 0 <= delta <= 7
+ if bucket == "next_15":
+ return 0 <= delta <= 15
+ if bucket == "this_month":
+ return due.year == today.year and due.month == today.month
+ return True
+
+
+def _due_status(due: date | None, today: date) -> tuple[str, str]:
+ if due is None:
+ return "none", "No due date"
+ delta = (due - today).days
+ if delta < 0:
+ days = abs(delta)
+ return "overdue", f"Overdue by {days} day{'s' if days != 1 else ''}"
+ if delta == 0:
+ return "today", "Due today"
+ if delta <= 7:
+ return "soon", f"Due in {delta} day{'s' if delta != 1 else ''}"
+ return "future", f"Due in {delta} days"
+
+
+def build_partner_payload(
+ db: Session,
+ request: Request,
+ current_user,
+ *,
+ q: str = "",
+ financial_year_filter: str | None = None,
+ period: str = "",
+ service_id: int | None = None,
+ due_bucket: str = "",
+ assignee_id: int | None = None,
+ priority: str = "",
+) -> dict[str, Any]:
roles = set(get_user_roles(db, current_user.id))
tenant_id, branch_id = _active_tenant_branch(request, current_user, roles)
- financial_year = _active_financial_year(request)
+ active_financial_year = _active_financial_year(request)
+ selected_fy = (financial_year_filter or "").strip()
+ if not selected_fy:
+ selected_fy = active_financial_year or ""
+ financial_year = None if selected_fy.lower() == "all" else (selected_fy or None)
today = date.today()
+ # Filter choices are drawn from the same partner/tenant/branch scope, without
+ # narrowing by FY so the board can move between years without changing the
+ # global application FY context.
+ option_stmt = select(ClientServiceSubscription).options(
+ selectinload(ClientServiceSubscription.client),
+ selectinload(ClientServiceSubscription.catalogue),
+ )
+ option_stmt = _subscription_scope_filter(option_stmt, tenant_id, branch_id, current_user, roles, financial_year=None)
+ option_engagements = db.execute(option_stmt).scalars().all()
+ financial_year_options = sorted({(row.financial_year or "").strip() for row in option_engagements if (row.financial_year or "").strip()}, reverse=True)
+ period_options = sorted({(row.period_label or "").strip() for row in option_engagements if (row.period_label or "").strip()})
+ service_map = {}
+ for row in option_engagements:
+ catalogue = getattr(row, "catalogue", None)
+ if catalogue:
+ service_map[int(catalogue.id)] = getattr(catalogue, "service_name", None) or getattr(catalogue, "name", None) or f"Service #{catalogue.id}"
+ service_options = [{"id": key, "name": service_map[key]} for key in sorted(service_map, key=lambda x: str(service_map[x]).casefold())]
+
task_stmt = (
select(ClientServiceTaskInstance)
.options(
selectinload(ClientServiceTaskInstance.client),
selectinload(ClientServiceTaskInstance.catalogue),
+ selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.client),
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.catalogue),
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.assigned_manager),
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.assigned_staff),
@@ -209,16 +431,15 @@ def build_partner_payload(db: Session, request: Request, current_user, *, q: str
)
)
task_stmt = _task_scope_filter(task_stmt, tenant_id, branch_id, current_user, roles, financial_year=financial_year)
- if q.strip():
- like = f"%{q.strip()}%"
- task_stmt = task_stmt.where(
- or_(
- ClientServiceTaskInstance.task_name.ilike(like),
- ClientServiceTaskInstance.description.ilike(like),
- ClientServiceTaskInstance.client.has(or_(Client.client_name.ilike(like), Client.client_code.ilike(like))),
- ClientServiceTaskInstance.catalogue.has(or_(ServiceCatalogue.service_name.ilike(like), ServiceCatalogue.service_code.ilike(like))),
- )
- )
+ if period.strip():
+ task_stmt = task_stmt.where(ClientServiceTaskInstance.subscription.has(ClientServiceSubscription.period_label == period.strip()))
+ if service_id:
+ task_stmt = task_stmt.where(ClientServiceTaskInstance.service_catalogue_id == int(service_id))
+ if assignee_id:
+ task_stmt = task_stmt.where(ClientServiceTaskInstance.assigned_to_user_id == int(assignee_id))
+ if priority.strip():
+ task_stmt = task_stmt.where(ClientServiceTaskInstance.priority == priority.strip())
+
tasks = db.execute(
task_stmt.order_by(
ClientServiceTaskInstance.internal_target_date.is_(None),
@@ -228,6 +449,57 @@ def build_partner_payload(db: Session, request: Request, current_user, *, q: str
)
).scalars().all()
+ subscriptions_by_id = {
+ int(task.subscription_id): task.subscription
+ for task in tasks
+ if getattr(task, "subscription_id", None) and getattr(task, "subscription", None)
+ }
+ scope_display_map = _partner_scope_display_map(db, tenant_id=tenant_id, subscriptions=list(subscriptions_by_id.values()))
+
+ # Team choices are limited to people actually present in the current scoped
+ # result set before search/due-date filtering.
+ team_map: dict[int, str] = {}
+ for task in tasks:
+ assignee = getattr(task, "assigned_to", None)
+ if assignee and getattr(assignee, "id", None):
+ team_map[int(assignee.id)] = getattr(assignee, "full_name", None) or getattr(assignee, "email", None) or f"User #{assignee.id}"
+ team_options = [{"id": key, "name": team_map[key]} for key in sorted(team_map, key=lambda x: str(team_map[x]).casefold())]
+
+ filtered_tasks: list[ClientServiceTaskInstance] = []
+ q_norm = _normalise_name(q)
+ for task in tasks:
+ _decorate_task(task, today)
+ subscription = getattr(task, "subscription", None)
+ display = scope_display_map.get(int(task.subscription_id), {}) if getattr(task, "subscription_id", None) else {}
+ task.business_display = display.get("primary") or task.client_display
+ task.legal_client_display = display.get("secondary_client") or ""
+ task.scope_context_display = display.get("scope_context") or ""
+ task.period_label_display = (getattr(subscription, "period_label", None) or "").strip() or "Annual / Full year"
+ task.financial_year_display = (getattr(subscription, "financial_year", None) or getattr(task, "financial_year", None) or "").strip()
+ task.engagement_due_date = getattr(subscription, "current_due_date", None)
+ task.engagement_original_due_date = getattr(subscription, "original_due_date", None)
+ task.engagement_start_date = getattr(subscription, "start_date", None)
+ task.engagement_end_date = getattr(subscription, "end_date", None)
+ task.due_status_code, task.due_status_label = _due_status(task.engagement_due_date, today)
+
+ if not _due_bucket_matches(task.engagement_due_date, due_bucket, today):
+ continue
+ if q_norm:
+ searchable = " ".join([
+ task.business_display or "",
+ task.legal_client_display or "",
+ task.client_display or "",
+ getattr(task, "task_name", None) or "",
+ getattr(task, "description", None) or "",
+ task.engagement_label or "",
+ task.period_label_display or "",
+ task.scope_context_display or "",
+ ])
+ if q_norm not in _normalise_name(searchable):
+ continue
+ filtered_tasks.append(task)
+ tasks = filtered_tasks
+
columns = [
{"code": "pending_review", "label": "Pending Review", "hint": "Completed tasks waiting for partner review", "tasks": []},
{"code": "clarification_required", "label": "Clarification Required", "hint": "Blocked tasks needing partner attention", "tasks": []},
@@ -239,7 +511,6 @@ def build_partner_payload(db: Session, request: Request, current_user, *, q: str
summary = {"total": len(tasks), "pending_review": 0, "blocked": 0, "overdue": 0, "due_today": 0, "completed": 0, "clients": set(), "engagements": set()}
for task in tasks:
- _decorate_task(task, today)
status = (task.status or "pending").lower()
if getattr(task, "client_id", None):
summary["clients"].add(task.client_id)
@@ -250,9 +521,9 @@ def build_partner_payload(db: Session, request: Request, current_user, *, q: str
summary["pending_review"] += 1
if status == "blocked":
summary["blocked"] += 1
- if getattr(task, "is_overdue", False):
+ if task.due_status_code == "overdue":
summary["overdue"] += 1
- if getattr(task, "is_due_today", False):
+ if task.due_status_code == "today":
summary["due_today"] += 1
if status in CLOSED_TASK_STATUSES:
summary["completed"] += 1
@@ -293,8 +564,29 @@ def build_partner_payload(db: Session, request: Request, current_user, *, q: str
due = getattr(engagement, "current_due_date", None)
engagement.is_overdue = bool(due and due < today and (engagement.status or "").lower() not in {"completed", "closed", "locked"})
- return {"summary": summary, "columns": columns, "engagements": engagements, "q": q, "today": today, "financial_year": financial_year}
-
+ return {
+ "summary": summary,
+ "columns": columns,
+ "engagements": engagements,
+ "q": q,
+ "today": today,
+ "financial_year": financial_year,
+ "active_financial_year": active_financial_year,
+ "filters": {
+ "financial_year": selected_fy or "all",
+ "period": period or "",
+ "service_id": int(service_id) if service_id else None,
+ "due_bucket": due_bucket or "",
+ "assignee_id": int(assignee_id) if assignee_id else None,
+ "priority": priority or "",
+ },
+ "filter_options": {
+ "financial_years": financial_year_options,
+ "periods": period_options,
+ "services": service_options,
+ "teams": team_options,
+ },
+ }
def _get_partner_task_or_redirect(db: Session, request: Request, current_user, task_id: int) -> ClientServiceTaskInstance | None:
roles = set(get_user_roles(db, current_user.id))
@@ -321,7 +613,17 @@ def partner_dashboard(request: Request, q: str = ""):
@router.get("/reviews")
-def partner_review_board(request: Request, q: str = ""):
+def partner_review_board(
+ request: Request,
+ q: str = "",
+ fy: str = "",
+ period: str = "",
+ service_id: str = "",
+ due: str = "",
+ assignee_id: str = "",
+ priority: str = "",
+ page_size: int = 10,
+):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
@@ -329,8 +631,29 @@ def partner_review_board(request: Request, q: str = ""):
return _redirect_login()
if not _is_partner_user(db, current_user):
return _redirect_denied()
- payload = build_partner_payload(db, request, current_user, q=q)
- return _render(request, "modules/partners/templates/partners/review_board.html", db, current_user, title="Partner Review Board", payload=payload, q=q, errors=[])
+ page_size = page_size if page_size in {10, 25, 50} else 10
+ service_id_value = int(service_id) if str(service_id).strip().isdigit() else None
+ assignee_id_value = int(assignee_id) if str(assignee_id).strip().isdigit() else None
+ payload = build_partner_payload(
+ db, request, current_user, q=q, financial_year_filter=fy, period=period,
+ service_id=service_id_value, due_bucket=due, assignee_id=assignee_id_value, priority=priority,
+ )
+ preserved = {
+ "q": q.strip(),
+ "fy": payload["filters"]["financial_year"],
+ "period": period.strip(),
+ "service_id": service_id_value or "",
+ "due": due.strip(),
+ "assignee_id": assignee_id_value or "",
+ "priority": priority.strip(),
+ "page_size": page_size,
+ }
+ filter_query = urlencode({k: v for k, v in preserved.items() if v not in (None, "")})
+ return _render(
+ request, "modules/partners/templates/partners/review_board.html", db, current_user,
+ title="Partner Review Board", payload=payload, q=q, page_size=page_size,
+ filter_query=filter_query, errors=[]
+ )
finally:
db.close()