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): 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( query = select(ServiceCatalogue).where(
ServiceCatalogue.is_active.is_(True) ServiceCatalogue.is_active.is_(True)
).options( ).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)) query = query.where(ServiceCatalogue.engagement_type == normalize_engagement_type(engagement_type))
total = db.execute(select(func.count()).select_from(query.subquery())).scalar_one() 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( rows = db.execute(
query.order_by(ServiceCatalogue.sort_order.asc(), ServiceCatalogue.service_name.asc()) query.order_by(ServiceCatalogue.sort_order.asc(), ServiceCatalogue.service_name.asc())
.offset((page - 1) * per_page) .offset((page - 1) * per_page)
.limit(per_page) .limit(per_page)
).scalars().all() ).scalars().all()
start_item = ((page - 1) * per_page + 1) if total else 0
end_item = min(page * per_page, total)
return { return {
"rows": rows, "rows": rows,
"q": q, "q": q,
@@ -105,7 +118,9 @@ def list_catalogue_payload(db: Session, *, q: str = "", category_id: int | None
"page": page, "page": page,
"per_page": per_page, "per_page": per_page,
"total": total, "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 %} {% for value, label in engagement_type_choices %}<option value="{{ value }}" {% if engagement_type == value %}selected{% endif %}>{{ label }}</option>{% endfor %}
</select> </select>
</div> </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> </form>
<div class="overflow-hidden rounded-2xl bg-white shadow-soft"> <div class="overflow-hidden rounded-2xl bg-white shadow-soft">
@@ -107,6 +122,72 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </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>
</div> </div>
{% endblock %} {% endblock %}
+2 -2
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timezone from datetime import datetime, timezone
from fastapi import APIRouter, File, Form, Request, UploadFile from fastapi import APIRouter, File, Form, Request, UploadFile
from fastapi.responses import FileResponse, RedirectResponse, StreamingResponse from fastapi.responses import FileResponse, RedirectResponse, StreamingResponse
from sqlalchemy import select 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") 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: finally:
db.close() db.close()