Add pagination to service catalogue
This commit is contained in:
@@ -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 %}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user