diff --git a/app/crud/augmented_part.py b/app/crud/augmented_part.py index 3257876..51a0bf7 100644 --- a/app/crud/augmented_part.py +++ b/app/crud/augmented_part.py @@ -24,6 +24,55 @@ async def list_all( return list(result.scalars().all()) +async def list_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, + skip: int = 0, + limit: int = 100, +) -> list[AugmentedPart]: + q = select(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.order_by(AugmentedPart.description).offset(skip).limit(limit)) + return list(result.scalars().all()) + + +async def get_unlinked_imported_parts_filtered( + db: AsyncSession, + description: str = "", + colour: str = "", +) -> list[ImportedPart]: + q = ( + select(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.order_by(ImportedPart.InventoryID)) + return list(result.scalars().all()) + + async def create( db: AsyncSession, *, diff --git a/app/crud/imported_part.py b/app/crud/imported_part.py index 3538de3..f595b88 100644 --- a/app/crud/imported_part.py +++ b/app/crud/imported_part.py @@ -1,4 +1,4 @@ -from sqlalchemy import insert, select, update +from sqlalchemy import distinct, insert, select, update from sqlalchemy.ext.asyncio import AsyncSession from app.models.imported_part import ImportedPart @@ -23,6 +23,28 @@ async def list_all( return list(result.scalars().all()) +async def list_filtered( + db: AsyncSession, + description: str = "", + skip: int = 0, + limit: int = 500, +) -> list[ImportedPart]: + q = select(ImportedPart) + if description.strip(): + q = q.where(ImportedPart.Description.ilike(f"%{description.strip()}%")) + result = await db.execute(q.order_by(ImportedPart.InventoryID).offset(skip).limit(limit)) + return list(result.scalars().all()) + + +async def distinct_colours(db: AsyncSession) -> list[str]: + result = await db.execute( + select(distinct(ImportedPart.TBXColour)) + .where(ImportedPart.TBXColour.isnot(None)) + .order_by(ImportedPart.TBXColour) + ) + return [row[0] for row in result.all()] + + async def count(db: AsyncSession) -> int: from sqlalchemy import func result = await db.execute(select(func.count()).select_from(ImportedPart)) diff --git a/app/static/css/style.css b/app/static/css/style.css index 1e4cc20..1dd886a 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -400,6 +400,39 @@ input[type=password] { .ap-item-conn { flex: 1; } .ap-item .row-actions { margin-left: auto; } +/* Filter bar */ +.filter-bar { + margin-bottom: 0.75rem; +} + +.filter-bar form { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} + +.filter-bar input[type="text"] { + flex: 1 1 180px; + min-width: 0; + padding: 0.35rem 0.6rem; + background: var(--surface-2); + border: 1px solid var(--border); + color: var(--text); + border-radius: 6px; + font: inherit; +} + +.filter-bar select { + flex: 0 1 auto; + padding: 0.35rem 0.6rem; + background: var(--surface-2); + border: 1px solid var(--border); + color: var(--text); + border-radius: 6px; + font: inherit; +} + /* Buttons */ .btn { font: inherit; diff --git a/app/templates/admin/_augmented_parts_table.html b/app/templates/admin/_augmented_parts_table.html index 1c9881c..034f175 100644 --- a/app/templates/admin/_augmented_parts_table.html +++ b/app/templates/admin/_augmented_parts_table.html @@ -5,28 +5,44 @@ {% if sync_error %}
{{ sync_error }}
{% endif %} -| ID | -SKU | -Description | -Colour | -Manufacturer | -Model | -Type | -Sub Type | -- |
|---|---|---|---|---|---|---|---|---|
| No augmented parts recorded. | ||||||||
| ID | +SKU | +Description | +Colour | +Manufacturer | +Model | +Type | +Sub Type | ++ |
|---|---|---|---|---|---|---|---|---|
| No augmented parts recorded. | ||||||||
{{ sync_error }}
{% endif %} -- Showing {{ parts|length }} record{{ '' if parts|length == 1 else 's' }}{% if total and total > parts|length %} of {{ total }} total{% endif %}. -
-| InventoryID | -Description | -TBX Colour | -Max Thread Depth (mm) | -Max Thread Depth (in) | -OD (mm) | -OD (in) | -Thread Attribute | -
|---|---|---|---|---|---|---|---|
| {{ p.InventoryID }} | -{{ p.Description or '—' }} | -{{ p.TBXColour or '—' }} | -{{ p.ProductMaxThreadDepthmm or '—' }} | -{{ p.ProductMaxThreadDepthin or '—' }} | -{{ p.ProductODmm or '—' }} | -{{ p.ProductODin or '—' }} | -{{ p.TBXThreadAttribute or '—' }} | -
| No imported parts. Use Refresh to sync from OData. | |||||||
+ Showing {{ parts|length }} record{{ '' if parts|length == 1 else 's' }}{% if total and total > parts|length %} of {{ total }} total{% endif %}. +
+| InventoryID | +Description | +TBX Colour | +Max Thread Depth (mm) | +Max Thread Depth (in) | +OD (mm) | +OD (in) | +Thread Attribute | +
|---|---|---|---|---|---|---|---|
| {{ p.InventoryID }} | +{{ p.Description or '—' }} | +{{ p.TBXColour or '—' }} | +{{ p.ProductMaxThreadDepthmm or '—' }} | +{{ p.ProductMaxThreadDepthin or '—' }} | +{{ p.ProductODmm or '—' }} | +{{ p.ProductODin or '—' }} | +{{ p.TBXThreadAttribute or '—' }} | +
| No imported parts. Use Refresh to sync from OData. | |||||||
{{ sync_result }}
{% endif %} {% if sync_error %}{{ sync_error }}
{% endif %} - {% if unlinked_parts %} + {% if any_unlinked %} {% else %}| + | Inventory ID | +Description | +Colour | +
|---|---|---|---|
| + | {{ ip.InventoryID }} |
+ {{ ip.Description | truncate(80) if ip.Description else '—' }} | +{{ ip.TBXColour or '—' }} | +
| No parts match the current filter. | |||