Restore client list pagination
This commit is contained in:
@@ -10,7 +10,7 @@ class ClientListFilters:
|
||||
partner_id: int | None = None
|
||||
include_archived: bool = False
|
||||
page: int = 1
|
||||
per_page: int = 10
|
||||
per_page: int = 25
|
||||
sort_by: str = "client_name"
|
||||
sort_order: str = "asc"
|
||||
|
||||
@@ -35,7 +35,7 @@ class ClientListFilters:
|
||||
partner_id=partner_id,
|
||||
include_archived=include_archived,
|
||||
page=max(int(kwargs.get("page", 1) or 1), 1),
|
||||
per_page=min(max(int(kwargs.get("per_page", 10) or 10), 1), 100),
|
||||
per_page=min(max(int(kwargs.get("per_page", 25) or 25), 1), 100),
|
||||
sort_by=kwargs.get("sort_by", "client_name") or "client_name",
|
||||
sort_order=kwargs.get("sort_order", "asc") or "asc",
|
||||
)
|
||||
|
||||
@@ -84,7 +84,7 @@ def list_clients(
|
||||
allow_cross_branch: bool = False, allow_all_clients: bool = False,
|
||||
partner_id: int | None = None, q: str = "", status: str = "", client_type: str = "",
|
||||
client_group_id: int | None = None, include_archived: bool = False,
|
||||
page: int = 1, per_page: int = 10, sort_by: str = "client_name", sort_order: str = "asc",
|
||||
page: int = 1, per_page: int = 25, sort_by: str = "client_name", sort_order: str = "asc",
|
||||
) -> dict:
|
||||
stmt = build_clients_query(
|
||||
tenant_id=tenant_id, branch_id=branch_id, allow_cross_branch=allow_cross_branch,
|
||||
@@ -92,7 +92,13 @@ def list_clients(
|
||||
client_type=client_type, client_group_id=client_group_id, include_archived=include_archived,
|
||||
)
|
||||
total = db.execute(select(func.count()).select_from(stmt.subquery())).scalar_one()
|
||||
result = db.execute(stmt.order_by(_safe_sort(sort_by, sort_order)).offset((page - 1) * per_page).limit(per_page)).all()
|
||||
per_page = min(max(int(per_page or 25), 1), 100)
|
||||
pages = max(ceil(total / per_page), 1)
|
||||
page = min(max(int(page or 1), 1), pages)
|
||||
offset = (page - 1) * per_page
|
||||
result = db.execute(
|
||||
stmt.order_by(_safe_sort(sort_by, sort_order)).offset(offset).limit(per_page)
|
||||
).all()
|
||||
rows=[]
|
||||
for client, partner_name, branch_name, tenant_name, client_group_name, client_group_code, association_type, assoc_firm_tenant_id, assoc_consultant_id, assoc_partner_user_id, assoc_created_source in result:
|
||||
row={**client.__dict__}; row.pop("_sa_instance_state",None)
|
||||
@@ -109,8 +115,7 @@ def list_clients(
|
||||
if branch_id and not allow_cross_branch: stats_stmt=stats_stmt.where(Client.branch_id==branch_id)
|
||||
if partner_id: stats_stmt=stats_stmt.where(Client.partner_id==partner_id)
|
||||
total_all,active,inactive,archived=db.execute(stats_stmt).one()
|
||||
pages=ceil(total/per_page) if per_page else 1
|
||||
return {"rows":rows,"meta":{"total":total,"page":page,"per_page":per_page,"pages":max(pages,1)},"stats":{"total":int(total_all or 0),"active":int(active or 0),"inactive":int(inactive or 0),"archived":int(archived or 0)}}
|
||||
return {"rows":rows,"meta":{"total":total,"page":page,"per_page":per_page,"pages":pages},"stats":{"total":int(total_all or 0),"active":int(active or 0),"inactive":int(inactive or 0),"archived":int(archived or 0)}}
|
||||
|
||||
|
||||
def get_client_detail_payload(db: Session, client_id: int):
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
<p class="text-sm text-slate-500">Association-aware list view.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3"><a href="/client-groups" class="inline-flex rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Client Groups</a>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<a href="/client-groups" class="inline-flex rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Client Groups</a>
|
||||
{% if can_export %}
|
||||
<a href="/clients/export?q={{ q }}&status={{ status }}&client_type={{ client_type }}&include_archived={{ include_archived }}&sort_by={{ sort_by }}&sort_order={{ sort_order }}"
|
||||
<a href="/clients/export?q={{ q|urlencode }}&status={{ status|urlencode }}&client_type={{ client_type|urlencode }}&partner_id={{ partner_id or '' }}&include_archived={{ include_archived }}&sort_by={{ sort_by|urlencode }}&sort_order={{ sort_order|urlencode }}"
|
||||
class="inline-flex rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">
|
||||
Export CSV
|
||||
</a>
|
||||
@@ -32,5 +33,82 @@
|
||||
</div>
|
||||
|
||||
{% include "modules/clients/templates/clients/partials/table.html" %}
|
||||
|
||||
{% if meta %}
|
||||
<div class="flex flex-col gap-4 rounded-2xl border border-slate-200 bg-white px-4 py-4 shadow-soft sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="text-sm text-slate-600">
|
||||
{% if meta.total > 0 %}
|
||||
Showing <span class="font-semibold text-slate-900">{{ ((meta.page - 1) * meta.per_page) + 1 }}</span>
|
||||
to <span class="font-semibold text-slate-900">{% if meta.page * meta.per_page > meta.total %}{{ meta.total }}{% else %}{{ meta.page * meta.per_page }}{% endif %}</span>
|
||||
of <span class="font-semibold text-slate-900">{{ meta.total }}</span> clients
|
||||
{% else %}
|
||||
No clients found
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<form method="get" action="/clients" class="flex items-center gap-2">
|
||||
<input type="hidden" name="q" value="{{ q }}">
|
||||
<input type="hidden" name="status" value="{{ status }}">
|
||||
<input type="hidden" name="client_type" value="{{ client_type }}">
|
||||
<input type="hidden" name="client_group_id" value="{{ client_group_id or '' }}">
|
||||
<input type="hidden" name="partner_id" value="{{ partner_id or '' }}">
|
||||
<input type="hidden" name="include_archived" value="{{ 'true' if include_archived else 'false' }}">
|
||||
<input type="hidden" name="sort_by" value="{{ sort_by }}">
|
||||
<input type="hidden" name="sort_order" value="{{ sort_order }}">
|
||||
<input type="hidden" name="page" value="1">
|
||||
<label for="clients-per-page" class="text-sm text-slate-600">Rows</label>
|
||||
<select id="clients-per-page" name="per_page" onchange="this.form.submit()"
|
||||
class="rounded-lg border border-slate-300 bg-white px-2 py-1.5 text-sm text-slate-700 focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100">
|
||||
{% for size in [10, 25, 50, 100] %}
|
||||
<option value="{{ size }}" {% if meta.per_page == size %}selected{% endif %}>{{ size }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
|
||||
{% if meta.pages > 1 %}
|
||||
<nav class="flex items-center gap-1" aria-label="Client list pagination">
|
||||
{% set common_query = 'q=' ~ (q|urlencode) ~ '&status=' ~ (status|urlencode) ~ '&client_type=' ~ (client_type|urlencode) ~ '&client_group_id=' ~ (client_group_id or '') ~ '&partner_id=' ~ (partner_id or '') ~ '&include_archived=' ~ ('true' if include_archived else 'false') ~ '&per_page=' ~ meta.per_page ~ '&sort_by=' ~ (sort_by|urlencode) ~ '&sort_order=' ~ (sort_order|urlencode) %}
|
||||
|
||||
{% if meta.page > 1 %}
|
||||
<a href="/clients?{{ common_query }}&page={{ meta.page - 1 }}"
|
||||
class="rounded-lg border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Previous</a>
|
||||
{% else %}
|
||||
<span class="cursor-not-allowed rounded-lg border border-slate-200 px-3 py-2 text-sm font-medium text-slate-400">Previous</span>
|
||||
{% endif %}
|
||||
|
||||
{% set start_page = meta.page - 2 if meta.page - 2 > 1 else 1 %}
|
||||
{% set end_page = meta.page + 2 if meta.page + 2 < meta.pages else meta.pages %}
|
||||
|
||||
{% if start_page > 1 %}
|
||||
<a href="/clients?{{ common_query }}&page=1" class="rounded-lg px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100">1</a>
|
||||
{% if start_page > 2 %}<span class="px-1 text-slate-400">…</span>{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% for page_number in range(start_page, end_page + 1) %}
|
||||
{% if page_number == meta.page %}
|
||||
<span aria-current="page" class="rounded-lg bg-brand-600 px-3 py-2 text-sm font-semibold text-white">{{ page_number }}</span>
|
||||
{% else %}
|
||||
<a href="/clients?{{ common_query }}&page={{ page_number }}"
|
||||
class="rounded-lg px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100">{{ page_number }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if end_page < meta.pages %}
|
||||
{% if end_page < meta.pages - 1 %}<span class="px-1 text-slate-400">…</span>{% endif %}
|
||||
<a href="/clients?{{ common_query }}&page={{ meta.pages }}" class="rounded-lg px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100">{{ meta.pages }}</a>
|
||||
{% endif %}
|
||||
|
||||
{% if meta.page < meta.pages %}
|
||||
<a href="/clients?{{ common_query }}&page={{ meta.page + 1 }}"
|
||||
class="rounded-lg border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Next</a>
|
||||
{% else %}
|
||||
<span class="cursor-not-allowed rounded-lg border border-slate-200 px-3 py-2 text-sm font-medium text-slate-400">Next</span>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,2 +1,55 @@
|
||||
|
||||
<div class="overflow-hidden rounded-2xl bg-white shadow-soft"><table class="min-w-full divide-y divide-slate-200"><thead class="bg-slate-50"><tr><th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Code</th><th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Client</th><th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Group</th><th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Association</th><th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Partner</th><th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Branch</th><th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Status</th><th class="px-4 py-3"></th></tr></thead><tbody class="divide-y divide-slate-100">{% for row in rows %}<tr><td class="px-4 py-3 text-sm font-medium text-slate-900">{{ row.client_code }}</td><td class="px-4 py-3 text-sm text-slate-700"><div class="font-medium">{{ row.client_name }}</div><div class="text-xs text-slate-500">{{ row.pan or row.gstin or '-' }}</div></td><td class="px-4 py-3 text-sm text-slate-700">{% if row.client_group_name %}<a href="/client-groups/{{ row.client_group_id }}" class="font-medium text-brand-700">{{ row.client_group_name }}</a><div class="text-xs text-slate-500">{{ row.group_relationship or row.client_group_code }}</div>{% else %}-{% endif %}</td><td class="px-4 py-3 text-sm text-slate-700"><div>{{ row.association_type or 'legacy_firm' }}</div><div class="text-xs text-slate-500">{{ row.assoc_created_source or 'legacy' }}</div></td><td class="px-4 py-3 text-sm text-slate-700">{{ row.partner_name or row.effective_partner_id or '-' }}</td><td class="px-4 py-3 text-sm text-slate-700">{{ row.branch_name or row.assoc_firm_branch_id or row.branch_id or '-' }}</td><td class="px-4 py-3 text-sm">{% if row.status == 'active' %}<span class="rounded-full bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-700">Active</span>{% elif row.status == 'archived' %}<span class="rounded-full bg-amber-100 px-2 py-1 text-xs font-medium text-amber-800">Archived</span>{% else %}<span class="rounded-full bg-slate-200 px-2 py-1 text-xs font-medium text-slate-700">Inactive</span>{% endif %}</td><td class="px-4 py-3 text-right"><a href="/clients/{{ row.id }}" class="text-sm font-medium text-brand-700 hover:underline">Open</a></td></tr>{% else %}<tr><td colspan="8" class="px-4 py-8 text-center text-sm text-slate-500">No clients found.</td></tr>{% endfor %}</tbody></table></div>
|
||||
<div class="overflow-x-auto rounded-2xl bg-white shadow-soft">
|
||||
<table class="min-w-full divide-y divide-slate-200">
|
||||
<thead class="bg-slate-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Code</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Client</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Group</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Association</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Partner</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Branch</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Status</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm font-medium text-slate-900">{{ row.client_code }}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-700">
|
||||
<div class="font-medium">{{ row.client_name }}</div>
|
||||
<div class="text-xs text-slate-500">{{ row.pan or row.gstin or '-' }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-700">
|
||||
{% if row.client_group_name %}
|
||||
<a href="/client-groups/{{ row.client_group_id }}" class="font-medium text-brand-700">{{ row.client_group_name }}</a>
|
||||
<div class="text-xs text-slate-500">{{ row.group_relationship or row.client_group_code }}</div>
|
||||
{% else %}-{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-700">
|
||||
<div>{{ row.association_type or 'legacy_firm' }}</div>
|
||||
<div class="text-xs text-slate-500">{{ row.assoc_created_source or 'legacy' }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-700">{{ row.partner_name or row.effective_partner_id or '-' }}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-700">{{ row.branch_name or row.assoc_firm_branch_id or row.branch_id or '-' }}</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-sm">
|
||||
{% if row.status == 'active' %}
|
||||
<span class="rounded-full bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-700">Active</span>
|
||||
{% elif row.status == 'archived' %}
|
||||
<span class="rounded-full bg-amber-100 px-2 py-1 text-xs font-medium text-amber-800">Archived</span>
|
||||
{% else %}
|
||||
<span class="rounded-full bg-slate-200 px-2 py-1 text-xs font-medium text-slate-700">Inactive</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-right">
|
||||
<a href="/clients/{{ row.id }}" class="text-sm font-medium text-brand-700 hover:underline">Open</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8" class="px-4 py-8 text-center text-sm text-slate-500">No clients found.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -297,7 +297,7 @@ def clients_list(
|
||||
partner_id: int | None = None,
|
||||
include_archived: bool = False,
|
||||
page: int = 1,
|
||||
per_page: int = 10,
|
||||
per_page: int = 25,
|
||||
sort_by: str = "client_name",
|
||||
sort_order: str = "asc",
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user