Adding pagination
This commit is contained in:
1 parent
a50fccd56d
commit
e923250177
8 files changed
+269
-46
No files matched your search
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import distinct, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.associations import augmented_part_imported_parts
|
||||
@@ -52,10 +52,39 @@ async def list_filtered(
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def count_filtered(
|
||||
db: AsyncSession,
|
||||
description: str = "",
|
||||
colour_id: int | None = None,
|
||||
manufacturer_id: int | None = None,
|
||||
scope_model_id: int | None = None,
|
||||
part_type_id: int | None = None,
|
||||
sub_part_type_id: int | None = None,
|
||||
) -> int:
|
||||
from sqlalchemy import func
|
||||
q = select(func.count()).select_from(AugmentedPart)
|
||||
if description.strip():
|
||||
q = q.where(AugmentedPart.description.ilike(f"%{description.strip()}%"))
|
||||
if colour_id is not None:
|
||||
q = q.where(AugmentedPart.colour_id == colour_id)
|
||||
if manufacturer_id is not None:
|
||||
q = q.where(AugmentedPart.manufacturer_id == manufacturer_id)
|
||||
if scope_model_id is not None:
|
||||
q = q.where(AugmentedPart.scope_model_id == scope_model_id)
|
||||
if part_type_id is not None:
|
||||
q = q.where(AugmentedPart.part_type_id == part_type_id)
|
||||
if sub_part_type_id is not None:
|
||||
q = q.where(AugmentedPart.sub_part_type_id == sub_part_type_id)
|
||||
result = await db.execute(q)
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
async def get_unlinked_imported_parts_filtered(
|
||||
db: AsyncSession,
|
||||
description: str = "",
|
||||
colour: str = "",
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> list[ImportedPart]:
|
||||
q = (
|
||||
select(ImportedPart)
|
||||
@@ -69,10 +98,48 @@ async def get_unlinked_imported_parts_filtered(
|
||||
q = q.where(ImportedPart.Description.ilike(f"%{description.strip()}%"))
|
||||
if colour.strip():
|
||||
q = q.where(ImportedPart.TBXColour.ilike(f"%{colour.strip()}%"))
|
||||
result = await db.execute(q.order_by(ImportedPart.InventoryID))
|
||||
result = await db.execute(q.order_by(ImportedPart.InventoryID).offset(skip).limit(limit))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def count_unlinked_filtered(
|
||||
db: AsyncSession,
|
||||
description: str = "",
|
||||
colour: str = "",
|
||||
) -> int:
|
||||
from sqlalchemy import func
|
||||
q = (
|
||||
select(func.count())
|
||||
.select_from(ImportedPart)
|
||||
.where(
|
||||
ImportedPart.InventoryID.not_in(
|
||||
select(augmented_part_imported_parts.c.imported_part_id)
|
||||
)
|
||||
)
|
||||
)
|
||||
if description.strip():
|
||||
q = q.where(ImportedPart.Description.ilike(f"%{description.strip()}%"))
|
||||
if colour.strip():
|
||||
q = q.where(ImportedPart.TBXColour.ilike(f"%{colour.strip()}%"))
|
||||
result = await db.execute(q)
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
async def distinct_unlinked_colours(db: AsyncSession) -> list[str]:
|
||||
q = (
|
||||
select(distinct(ImportedPart.TBXColour))
|
||||
.where(
|
||||
ImportedPart.TBXColour.isnot(None),
|
||||
ImportedPart.InventoryID.not_in(
|
||||
select(augmented_part_imported_parts.c.imported_part_id)
|
||||
),
|
||||
)
|
||||
.order_by(ImportedPart.TBXColour)
|
||||
)
|
||||
result = await db.execute(q)
|
||||
return [row[0] for row in result.all()]
|
||||
|
||||
|
||||
async def create(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
|
||||
@@ -51,6 +51,15 @@ async def count(db: AsyncSession) -> int:
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
async def count_filtered(db: AsyncSession, description: str = "") -> int:
|
||||
from sqlalchemy import func
|
||||
q = select(func.count()).select_from(ImportedPart)
|
||||
if description.strip():
|
||||
q = q.where(ImportedPart.Description.ilike(f"%{description.strip()}%"))
|
||||
result = await db.execute(q)
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
async def delete(db: AsyncSession, part: ImportedPart) -> None:
|
||||
await db.delete(part)
|
||||
await db.commit()
|
||||
|
||||
@@ -400,6 +400,27 @@ input[type=password] {
|
||||
.ap-item-conn { flex: 1; }
|
||||
.ap-item .row-actions { margin-left: auto; }
|
||||
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Filter bar */
|
||||
.filter-bar {
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<div id="augmented-parts-table-card" class="card">
|
||||
{% include "admin/_pagination.html" %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<div id="imported-parts-table-card" class="card" style="overflow-x:auto">
|
||||
<p class="muted" style="margin-bottom:.5rem">
|
||||
Showing {{ parts|length }} record{{ '' if parts|length == 1 else 's' }}{% if total and total > parts|length %} of {{ total }} total{% endif %}.
|
||||
</p>
|
||||
{% include "admin/_pagination.html" %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{% if pagination %}
|
||||
<div class="pagination">
|
||||
<span class="muted pagination-info">
|
||||
{% if pagination.total == 0 %}
|
||||
No records found.
|
||||
{% elif pagination.total_pages == 1 %}
|
||||
Showing {{ pagination.total }} record{{ '' if pagination.total == 1 else 's' }}.
|
||||
{% else %}
|
||||
Showing {{ pagination.showing_from }}–{{ pagination.showing_to }} of {{ pagination.total }}.
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if pagination.total_pages > 1 %}
|
||||
<div class="pagination-nav">
|
||||
{% if pagination.prev_url %}
|
||||
<button class="btn btn-ghost btn-sm"
|
||||
hx-get="{{ pagination.prev_url }}"
|
||||
hx-target="{{ pagination.target_id }}"
|
||||
hx-swap="outerHTML">← Prev</button>
|
||||
{% else %}
|
||||
<button class="btn btn-ghost btn-sm" disabled>← Prev</button>
|
||||
{% endif %}
|
||||
<span class="muted">{{ pagination.page }} / {{ pagination.total_pages }}</span>
|
||||
{% if pagination.next_url %}
|
||||
<button class="btn btn-ghost btn-sm"
|
||||
hx-get="{{ pagination.next_url }}"
|
||||
hx-target="{{ pagination.target_id }}"
|
||||
hx-swap="outerHTML">Next →</button>
|
||||
{% else %}
|
||||
<button class="btn btn-ghost btn-sm" disabled>Next →</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,4 +1,5 @@
|
||||
<div id="skipped-parts-table-card" class="card" style="overflow-x:auto">
|
||||
{% include "admin/_pagination.html" %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
+133
-41
@@ -1,5 +1,8 @@
|
||||
"""HTMX-driven admin dashboard for users, roles, and thread standards."""
|
||||
|
||||
import math
|
||||
import urllib.parse
|
||||
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -31,6 +34,40 @@ from app.web.templates import templates
|
||||
|
||||
router = APIRouter(prefix="/admin", include_in_schema=False)
|
||||
|
||||
_PAGE_SIZE = 50
|
||||
|
||||
|
||||
def _paginate(
|
||||
base_path: str,
|
||||
filter_params: dict,
|
||||
page: int,
|
||||
page_size: int,
|
||||
total: int,
|
||||
target_id: str,
|
||||
) -> dict:
|
||||
total_pages = max(1, math.ceil(total / page_size))
|
||||
page = max(1, min(page, total_pages))
|
||||
skip = (page - 1) * page_size
|
||||
|
||||
def _url(p: int) -> str:
|
||||
params = {k: v for k, v in filter_params.items() if v not in (None, "", 0)}
|
||||
params["page"] = p
|
||||
params["page_size"] = page_size
|
||||
return f"{base_path}?{urllib.parse.urlencode(params)}"
|
||||
|
||||
return {
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total": total,
|
||||
"total_pages": total_pages,
|
||||
"skip": skip,
|
||||
"showing_from": skip + 1 if total > 0 else 0,
|
||||
"showing_to": min(skip + page_size, total),
|
||||
"prev_url": _url(page - 1) if page > 1 else None,
|
||||
"next_url": _url(page + 1) if page < total_pages else None,
|
||||
"target_id": target_id,
|
||||
}
|
||||
|
||||
|
||||
def web_require(*perms: str):
|
||||
"""Web dependency: allow superusers or holders of all given permissions."""
|
||||
@@ -1045,12 +1082,13 @@ async def imported_parts_page(
|
||||
"username": config.username if config else None,
|
||||
"has_password": bool(config and config.password),
|
||||
}
|
||||
parts = await imported_part_crud.list_all(db)
|
||||
total = await imported_part_crud.count(db)
|
||||
parts = await imported_part_crud.list_filtered(db, limit=_PAGE_SIZE)
|
||||
pagination = _paginate("/admin/imported-parts/filter", {}, 1, _PAGE_SIZE, total, "#imported-parts-table-card")
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/imported_parts.html",
|
||||
{"user": user, "config": config_view, "parts": parts, "total": total},
|
||||
{"user": user, "config": config_view, "parts": parts, "pagination": pagination},
|
||||
)
|
||||
|
||||
|
||||
@@ -1087,13 +1125,17 @@ async def refresh_imported_parts(
|
||||
actor: User = Depends(web_require("imported_parts:write")),
|
||||
):
|
||||
config = await imported_part_config_crud.get(db)
|
||||
if not config or not config.odata_url:
|
||||
parts = await imported_part_crud.list_all(db)
|
||||
|
||||
async def _imported_table_ctx(sync_result=None, sync_error=None):
|
||||
total = await imported_part_crud.count(db)
|
||||
parts = await imported_part_crud.list_filtered(db, limit=_PAGE_SIZE)
|
||||
pagination = _paginate("/admin/imported-parts/filter", {}, 1, _PAGE_SIZE, total, "#imported-parts-table-card")
|
||||
return {"parts": parts, "pagination": pagination, "sync_result": sync_result, "sync_error": sync_error}
|
||||
|
||||
if not config or not config.odata_url:
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_imported_parts_table.html",
|
||||
{"parts": parts, "total": total, "sync_error": "OData URL is not configured."},
|
||||
request, "admin/_imported_parts_table.html",
|
||||
await _imported_table_ctx(sync_error="OData URL is not configured."),
|
||||
)
|
||||
try:
|
||||
records = await odata_service.fetch(config.odata_url, config.username, config.password)
|
||||
@@ -1103,24 +1145,14 @@ async def refresh_imported_parts(
|
||||
resource_id=None,
|
||||
description=f"OData sync: {created} created, {updated} updated ({len(records)} total records)",
|
||||
)
|
||||
parts = await imported_part_crud.list_all(db)
|
||||
total = await imported_part_crud.count(db)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_imported_parts_table.html",
|
||||
{
|
||||
"parts": parts,
|
||||
"total": total,
|
||||
"sync_result": f"Sync complete: {created} created, {updated} updated.",
|
||||
},
|
||||
request, "admin/_imported_parts_table.html",
|
||||
await _imported_table_ctx(sync_result=f"Sync complete: {created} created, {updated} updated."),
|
||||
)
|
||||
except Exception as exc:
|
||||
parts = await imported_part_crud.list_all(db)
|
||||
total = await imported_part_crud.count(db)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_imported_parts_table.html",
|
||||
{"parts": parts, "total": total, "sync_error": f"Sync failed: {exc}"},
|
||||
request, "admin/_imported_parts_table.html",
|
||||
await _imported_table_ctx(sync_error=f"Sync failed: {exc}"),
|
||||
)
|
||||
|
||||
|
||||
@@ -1128,20 +1160,35 @@ async def refresh_imported_parts(
|
||||
async def filter_imported_parts(
|
||||
request: Request,
|
||||
description: str = "",
|
||||
page: int = 1,
|
||||
page_size: int = _PAGE_SIZE,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("imported_parts:read")),
|
||||
):
|
||||
parts = await imported_part_crud.list_filtered(db, description=description)
|
||||
total = await imported_part_crud.count_filtered(db, description=description)
|
||||
pagination = _paginate(
|
||||
"/admin/imported-parts/filter",
|
||||
{"description": description},
|
||||
page, page_size, total,
|
||||
"#imported-parts-table-card",
|
||||
)
|
||||
parts = await imported_part_crud.list_filtered(
|
||||
db, description=description, skip=pagination["skip"], limit=page_size,
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_imported_parts_table_card.html",
|
||||
{"parts": parts},
|
||||
{"parts": parts, "pagination": pagination},
|
||||
)
|
||||
|
||||
|
||||
# --- Augmented Parts ---------------------------------------------------------
|
||||
|
||||
|
||||
async def _skipped_colours(db: AsyncSession) -> list[str]:
|
||||
return await augmented_part_crud.distinct_unlinked_colours(db)
|
||||
|
||||
|
||||
async def _augmented_part_form_ctx(db: AsyncSession) -> dict:
|
||||
return {
|
||||
"part_colours": await part_colour_crud.list_all(db),
|
||||
@@ -1159,8 +1206,10 @@ async def augmented_parts_page(
|
||||
user: User = Depends(web_require("augmented_parts:read")),
|
||||
):
|
||||
ctx = await _augmented_part_form_ctx(db)
|
||||
total = await augmented_part_crud.count_filtered(db)
|
||||
ctx["augmented_parts"] = await augmented_part_crud.list_filtered(db, limit=_PAGE_SIZE)
|
||||
ctx["pagination"] = _paginate("/admin/augmented-parts/filter", {}, 1, _PAGE_SIZE, total, "#augmented-parts-table-card")
|
||||
ctx["user"] = user
|
||||
ctx["augmented_parts"] = await augmented_part_crud.list_all(db)
|
||||
return templates.TemplateResponse(request, "admin/augmented_parts.html", ctx)
|
||||
|
||||
|
||||
@@ -1195,10 +1244,12 @@ async def create_augmented_part(
|
||||
),
|
||||
)
|
||||
form_ctx = await _augmented_part_form_ctx(db)
|
||||
total = await augmented_part_crud.count_filtered(db)
|
||||
pagination = _paginate("/admin/augmented-parts/filter", {}, 1, _PAGE_SIZE, total, "#augmented-parts-table-card")
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_augmented_parts_table.html",
|
||||
{**form_ctx, "augmented_parts": await augmented_part_crud.list_all(db)},
|
||||
{**form_ctx, "augmented_parts": await augmented_part_crud.list_filtered(db, limit=_PAGE_SIZE), "pagination": pagination},
|
||||
)
|
||||
|
||||
|
||||
@@ -1228,12 +1279,15 @@ async def sync_augmented_parts(
|
||||
sync_result = None
|
||||
sync_error = f"Sync failed: {exc}"
|
||||
form_ctx = await _augmented_part_form_ctx(db)
|
||||
total = await augmented_part_crud.count_filtered(db)
|
||||
pagination = _paginate("/admin/augmented-parts/filter", {}, 1, _PAGE_SIZE, total, "#augmented-parts-table-card")
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_augmented_parts_table.html",
|
||||
{
|
||||
**form_ctx,
|
||||
"augmented_parts": await augmented_part_crud.list_all(db),
|
||||
"augmented_parts": await augmented_part_crud.list_filtered(db, limit=_PAGE_SIZE),
|
||||
"pagination": pagination,
|
||||
"sync_result": sync_result,
|
||||
"sync_error": sync_error,
|
||||
},
|
||||
@@ -1249,22 +1303,49 @@ async def filter_augmented_parts(
|
||||
scope_model_id: str = "",
|
||||
part_type_id: str = "",
|
||||
sub_part_type_id: str = "",
|
||||
page: int = 1,
|
||||
page_size: int = _PAGE_SIZE,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("augmented_parts:read")),
|
||||
):
|
||||
colour_id_int = int(colour_id) if colour_id else None
|
||||
manufacturer_id_int = int(manufacturer_id) if manufacturer_id else None
|
||||
scope_model_id_int = int(scope_model_id) if scope_model_id else None
|
||||
part_type_id_int = int(part_type_id) if part_type_id else None
|
||||
sub_part_type_id_int = int(sub_part_type_id) if sub_part_type_id else None
|
||||
total = await augmented_part_crud.count_filtered(
|
||||
db,
|
||||
description=description,
|
||||
colour_id=colour_id_int,
|
||||
manufacturer_id=manufacturer_id_int,
|
||||
scope_model_id=scope_model_id_int,
|
||||
part_type_id=part_type_id_int,
|
||||
sub_part_type_id=sub_part_type_id_int,
|
||||
)
|
||||
filter_params = {
|
||||
"description": description,
|
||||
"colour_id": colour_id,
|
||||
"manufacturer_id": manufacturer_id,
|
||||
"scope_model_id": scope_model_id,
|
||||
"part_type_id": part_type_id,
|
||||
"sub_part_type_id": sub_part_type_id,
|
||||
}
|
||||
pagination = _paginate("/admin/augmented-parts/filter", filter_params, page, page_size, total, "#augmented-parts-table-card")
|
||||
parts = await augmented_part_crud.list_filtered(
|
||||
db,
|
||||
description=description,
|
||||
colour_id=int(colour_id) if colour_id else None,
|
||||
manufacturer_id=int(manufacturer_id) if manufacturer_id else None,
|
||||
scope_model_id=int(scope_model_id) if scope_model_id else None,
|
||||
part_type_id=int(part_type_id) if part_type_id else None,
|
||||
sub_part_type_id=int(sub_part_type_id) if sub_part_type_id else None,
|
||||
colour_id=colour_id_int,
|
||||
manufacturer_id=manufacturer_id_int,
|
||||
scope_model_id=scope_model_id_int,
|
||||
part_type_id=part_type_id_int,
|
||||
sub_part_type_id=sub_part_type_id_int,
|
||||
skip=pagination["skip"],
|
||||
limit=page_size,
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_augmented_parts_table_card.html",
|
||||
{"augmented_parts": parts},
|
||||
{"augmented_parts": parts, "pagination": pagination},
|
||||
)
|
||||
|
||||
|
||||
@@ -1275,8 +1356,10 @@ async def skipped_parts_page(
|
||||
user: User = Depends(web_require("augmented_parts:read")),
|
||||
):
|
||||
form_ctx = await _augmented_part_form_ctx(db)
|
||||
unlinked = await augmented_part_crud.get_unlinked_imported_parts(db)
|
||||
skipped_colours = sorted({p.TBXColour for p in unlinked if p.TBXColour})
|
||||
total_unlinked = await augmented_part_crud.count_unlinked_filtered(db)
|
||||
skipped_colours = await _skipped_colours(db)
|
||||
pagination = _paginate("/admin/augmented-parts/skipped/filter", {}, 1, _PAGE_SIZE, total_unlinked, "#skipped-parts-table-card")
|
||||
unlinked = await augmented_part_crud.get_unlinked_imported_parts_filtered(db, limit=_PAGE_SIZE)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/augmented_parts_skipped.html",
|
||||
@@ -1284,10 +1367,11 @@ async def skipped_parts_page(
|
||||
"user": user,
|
||||
**form_ctx,
|
||||
"unlinked_parts": unlinked,
|
||||
"any_unlinked": bool(unlinked),
|
||||
"any_unlinked": bool(total_unlinked),
|
||||
"skipped_colours": skipped_colours,
|
||||
"filter_description": "",
|
||||
"filter_colour": "",
|
||||
"pagination": pagination,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1340,18 +1424,21 @@ async def sync_skipped_parts(
|
||||
sync_result = f"{synced} part(s) synced successfully."
|
||||
|
||||
form_ctx = await _augmented_part_form_ctx(db)
|
||||
unlinked = await augmented_part_crud.get_unlinked_imported_parts(db)
|
||||
skipped_colours = sorted({p.TBXColour for p in unlinked if p.TBXColour})
|
||||
total_unlinked = await augmented_part_crud.count_unlinked_filtered(db)
|
||||
colours = await _skipped_colours(db)
|
||||
pagination = _paginate("/admin/augmented-parts/skipped/filter", {}, 1, _PAGE_SIZE, total_unlinked, "#skipped-parts-table-card")
|
||||
unlinked = await augmented_part_crud.get_unlinked_imported_parts_filtered(db, limit=_PAGE_SIZE)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_skipped_parts_table.html",
|
||||
{
|
||||
**form_ctx,
|
||||
"unlinked_parts": unlinked,
|
||||
"any_unlinked": bool(unlinked),
|
||||
"skipped_colours": skipped_colours,
|
||||
"any_unlinked": bool(total_unlinked),
|
||||
"skipped_colours": colours,
|
||||
"filter_description": "",
|
||||
"filter_colour": "",
|
||||
"pagination": pagination,
|
||||
"sync_result": sync_result,
|
||||
"sync_error": sync_error,
|
||||
},
|
||||
@@ -1363,16 +1450,21 @@ async def filter_skipped_parts(
|
||||
request: Request,
|
||||
description: str = "",
|
||||
colour: str = "",
|
||||
page: int = 1,
|
||||
page_size: int = _PAGE_SIZE,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("augmented_parts:read")),
|
||||
):
|
||||
total = await augmented_part_crud.count_unlinked_filtered(db, description=description, colour=colour)
|
||||
filter_params = {"description": description, "colour": colour}
|
||||
pagination = _paginate("/admin/augmented-parts/skipped/filter", filter_params, page, page_size, total, "#skipped-parts-table-card")
|
||||
unlinked = await augmented_part_crud.get_unlinked_imported_parts_filtered(
|
||||
db, description=description, colour=colour
|
||||
db, description=description, colour=colour, skip=pagination["skip"], limit=page_size,
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_skipped_parts_table_card.html",
|
||||
{"unlinked_parts": unlinked},
|
||||
{"unlinked_parts": unlinked, "pagination": pagination},
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user