Add pagination to service catalogue

This commit is contained in:
A R R R Associates
2026-07-18 13:42:47 +05:30
parent cd8e679d71
commit a03314af50
3 changed files with 100 additions and 4 deletions
+16 -1
View File
@@ -65,6 +65,13 @@ def get_category(db: Session, category_id: int) -> ServiceCategory | None:
def list_catalogue_payload(db: Session, *, q: str = "", category_id: int | None = None, recurrence_type: str = "", engagement_type: str = "", page: int = 1, per_page: int = 20):
# Keep catalogue paging predictable and prevent invalid/very large requests.
page = max(1, int(page or 1))
allowed_page_sizes = {20, 50, 100}
per_page = int(per_page or 20)
if per_page not in allowed_page_sizes:
per_page = 20
query = select(ServiceCatalogue).where(
ServiceCatalogue.is_active.is_(True)
).options(
@@ -90,12 +97,18 @@ def list_catalogue_payload(db: Session, *, q: str = "", category_id: int | None
query = query.where(ServiceCatalogue.engagement_type == normalize_engagement_type(engagement_type))
total = db.execute(select(func.count()).select_from(query.subquery())).scalar_one()
pages = max(1, (total + per_page - 1) // per_page)
page = min(page, pages)
rows = db.execute(
query.order_by(ServiceCatalogue.sort_order.asc(), ServiceCatalogue.service_name.asc())
.offset((page - 1) * per_page)
.limit(per_page)
).scalars().all()
start_item = ((page - 1) * per_page + 1) if total else 0
end_item = min(page * per_page, total)
return {
"rows": rows,
"q": q,
@@ -105,7 +118,9 @@ def list_catalogue_payload(db: Session, *, q: str = "", category_id: int | None
"page": page,
"per_page": per_page,
"total": total,
"pages": max(1, (total + per_page - 1) // per_page),
"pages": pages,
"start_item": start_item,
"end_item": end_item,
}
@@ -41,7 +41,22 @@
{% for value, label in engagement_type_choices %}<option value="{{ value }}" {% if engagement_type == value %}selected{% endif %}>{{ label }}</option>{% endfor %}
</select>
</div>
<div class="mt-3 flex justify-end"><button class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium hover:bg-slate-50">Search</button></div>
<div class="mt-3 flex flex-wrap items-center justify-between gap-3">
<label class="flex items-center gap-2 text-sm text-slate-600">
Rows per page
<select name="per_page" class="rounded-lg border border-slate-300 px-3 py-2 text-sm">
{% for size in [20, 50, 100] %}
<option value="{{ size }}" {% if per_page == size %}selected{% endif %}>{{ size }}</option>
{% endfor %}
</select>
</label>
<div class="flex gap-2">
{% if q or category_id or recurrence_type or engagement_type or per_page != 20 %}
<a href="/services/catalogue" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Clear</a>
{% endif %}
<button class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium hover:bg-slate-50">Search</button>
</div>
</div>
</form>
<div class="overflow-hidden rounded-2xl bg-white shadow-soft">
@@ -107,6 +122,72 @@
{% endfor %}
</tbody>
</table>
<div class="flex flex-col gap-3 border-t border-slate-200 px-4 py-4 sm:flex-row sm:items-center sm:justify-between">
<div class="text-sm text-slate-600">
{% if total %}
Showing <span class="font-semibold text-slate-900">{{ start_item }}</span><span class="font-semibold text-slate-900">{{ end_item }}</span>
of <span class="font-semibold text-slate-900">{{ total }}</span> active services
{% else %}
No active services found
{% endif %}
</div>
{% if pages > 1 %}
<div class="flex flex-wrap items-center justify-end gap-2" aria-label="Service catalogue pagination">
<form method="get">
<input type="hidden" name="q" value="{{ q }}">
<input type="hidden" name="category_id" value="{{ category_id or '' }}">
<input type="hidden" name="recurrence_type" value="{{ recurrence_type }}">
<input type="hidden" name="engagement_type" value="{{ engagement_type }}">
<input type="hidden" name="per_page" value="{{ per_page }}">
<input type="hidden" name="page" value="{{ page - 1 }}">
<button {% if page <= 1 %}disabled{% endif %} class="rounded-lg border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40">Previous</button>
</form>
{% set first_page = 1 if page <= 3 else page - 2 %}
{% set last_page = pages if page + 2 >= pages else page + 2 %}
{% if first_page > 1 %}
<form method="get">
<input type="hidden" name="q" value="{{ q }}"><input type="hidden" name="category_id" value="{{ category_id or '' }}"><input type="hidden" name="recurrence_type" value="{{ recurrence_type }}"><input type="hidden" name="engagement_type" value="{{ engagement_type }}"><input type="hidden" name="per_page" value="{{ per_page }}"><input type="hidden" name="page" value="1">
<button class="rounded-lg border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">1</button>
</form>
{% if first_page > 2 %}<span class="px-1 text-slate-400"></span>{% endif %}
{% endif %}
{% for page_number in range(first_page, last_page + 1) %}
<form method="get">
<input type="hidden" name="q" value="{{ q }}">
<input type="hidden" name="category_id" value="{{ category_id or '' }}">
<input type="hidden" name="recurrence_type" value="{{ recurrence_type }}">
<input type="hidden" name="engagement_type" value="{{ engagement_type }}">
<input type="hidden" name="per_page" value="{{ per_page }}">
<input type="hidden" name="page" value="{{ page_number }}">
<button class="rounded-lg border px-3 py-2 text-sm font-medium {% if page_number == page %}border-brand-600 bg-brand-600 text-white{% else %}border-slate-300 text-slate-700 hover:bg-slate-50{% endif %}" {% if page_number == page %}aria-current="page"{% endif %}>{{ page_number }}</button>
</form>
{% endfor %}
{% if last_page < pages %}
{% if last_page < pages - 1 %}<span class="px-1 text-slate-400"></span>{% endif %}
<form method="get">
<input type="hidden" name="q" value="{{ q }}"><input type="hidden" name="category_id" value="{{ category_id or '' }}"><input type="hidden" name="recurrence_type" value="{{ recurrence_type }}"><input type="hidden" name="engagement_type" value="{{ engagement_type }}"><input type="hidden" name="per_page" value="{{ per_page }}"><input type="hidden" name="page" value="{{ pages }}">
<button class="rounded-lg border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">{{ pages }}</button>
</form>
{% endif %}
<form method="get">
<input type="hidden" name="q" value="{{ q }}">
<input type="hidden" name="category_id" value="{{ category_id or '' }}">
<input type="hidden" name="recurrence_type" value="{{ recurrence_type }}">
<input type="hidden" name="engagement_type" value="{{ engagement_type }}">
<input type="hidden" name="per_page" value="{{ per_page }}">
<input type="hidden" name="page" value="{{ page + 1 }}">
<button {% if page >= pages %}disabled{% endif %} class="rounded-lg border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40">Next</button>
</form>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
+2 -2
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
from __future__ import annotations
from datetime import datetime, timezone
from fastapi import APIRouter, File, Form, Request, UploadFile
from fastapi.responses import FileResponse, RedirectResponse, StreamingResponse
from sqlalchemy import select
@@ -1621,4 +1622,3 @@ def upload_firm_task_templates(request: Request, file: UploadFile = File(...), u
return _render(request, "modules/services/templates/services/bulk_imports/result.html", db, user, title="Firm Task Templates Import Result", result=result, back_url="/services/bulk-imports")
finally:
db.close()