diff --git a/app/templates/_nav.html b/app/templates/_nav.html index fb82ea3..97225e0 100644 --- a/app/templates/_nav.html +++ b/app/templates/_nav.html @@ -32,6 +32,9 @@ {% if user.is_superuser or "augmented_parts:read" in user.permissions %} Augmented Parts {% endif %} + {% if user.is_superuser or "augmented_parts:write" in user.permissions %} + Skipped Parts + {% endif %} {% if user.is_superuser or "part_colours:read" in user.permissions %} Part Colours {% endif %} diff --git a/app/templates/admin/_skipped_parts_table.html b/app/templates/admin/_skipped_parts_table.html new file mode 100644 index 0000000..7a80033 --- /dev/null +++ b/app/templates/admin/_skipped_parts_table.html @@ -0,0 +1,61 @@ +
+ {% if sync_result %}

{{ sync_result }}

{% endif %} + {% if sync_error %}

{{ sync_error }}

{% endif %} + + {% if unlinked_parts %} +
+ +
+
+ + + +
+
+ +
+ + + + + + + + + + + {% for ip in unlinked_parts %} + + + + + + + {% endfor %} + +
Inventory IDDescriptionColour
{{ ip.InventoryID }}{{ ip.Description | truncate(80) if ip.Description else '—' }}{{ ip.TBXColour or '—' }}
+
+ +
+ {% else %} +
+

No skipped parts — all imported parts have been synchronised.

+
+ {% endif %} +
diff --git a/app/templates/admin/augmented_parts.html b/app/templates/admin/augmented_parts.html index 1fcc735..a6a6140 100644 --- a/app/templates/admin/augmented_parts.html +++ b/app/templates/admin/augmented_parts.html @@ -57,11 +57,14 @@

Sync with Imported Parts

Creates Augmented Part entries for new Imported Parts, and removes entries for deleted Imported Parts.

-
- -
+
+
+ +
+ View skipped parts → +
- {% include "admin/_augmented_parts_sections.html" %} + {% include "admin/_augmented_parts_table.html" %} {% endblock %} diff --git a/app/templates/admin/augmented_parts_skipped.html b/app/templates/admin/augmented_parts_skipped.html new file mode 100644 index 0000000..138b9c6 --- /dev/null +++ b/app/templates/admin/augmented_parts_skipped.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} +{% block title %}Skipped Parts · {{ app_name }}{% endblock %} +{% block body %} +{% include "_nav.html" %} +
+

Skipped Imported Parts

+

+ These parts were skipped during sync because no Part Type could be inferred + from their description. Select one or more rows, choose a Type (and optionally + a Sub Type), then click Sync selected to create their + Augmented Part entries. +

+

← Augmented Parts

+ + {% include "admin/_skipped_parts_table.html" %} +
+ + +{% endblock %} diff --git a/app/web/admin.py b/app/web/admin.py index 034ba19..97ecaf6 100644 --- a/app/web/admin.py +++ b/app/web/admin.py @@ -1143,7 +1143,6 @@ async def augmented_parts_page( ctx = await _augmented_part_form_ctx(db) ctx["user"] = user ctx["augmented_parts"] = await augmented_part_crud.list_all(db) - ctx["unlinked_parts"] = await augmented_part_crud.get_unlinked_imported_parts(db) return templates.TemplateResponse(request, "admin/augmented_parts.html", ctx) @@ -1209,56 +1208,91 @@ async def sync_augmented_parts( except Exception as exc: sync_result = None sync_error = f"Sync failed: {exc}" - form_ctx = await _augmented_part_form_ctx(db) return templates.TemplateResponse( request, - "admin/_augmented_parts_sections.html", + "admin/_augmented_parts_table.html", { - **form_ctx, "augmented_parts": await augmented_part_crud.list_all(db), - "unlinked_parts": await augmented_part_crud.get_unlinked_imported_parts(db), "sync_result": sync_result, "sync_error": sync_error, }, ) -@router.post("/augmented-parts/unlinked", response_class=HTMLResponse) -async def sync_unlinked_part( +@router.get("/augmented-parts/skipped", response_class=HTMLResponse) +async def skipped_parts_page( request: Request, - inventory_id: str = Form(...), - part_type_id: int = Form(...), - sub_part_type_id: str = Form(""), db: AsyncSession = Depends(get_db), - actor: User = Depends(web_require("augmented_parts:write")), + user: User = Depends(web_require("augmented_parts:read")), ): - try: - ap = await augmented_part_sync_service.sync_one( - db, - inventory_id=inventory_id, - part_type_id=part_type_id, - sub_part_type_id=int(sub_part_type_id) if sub_part_type_id else None, - ) - await audit_crud.log( - db, actor=actor, action="create", resource_type="AugmentedPart", - resource_id=ap.id, - description=audit_crud.describe_create("AugmentedPart", ap.id, ap.description), - ) - unlinked_sync_result = f"Created Augmented Part for '{ap.description}'." - unlinked_sync_error = None - except Exception as exc: - unlinked_sync_result = None - unlinked_sync_error = f"Sync failed: {exc}" form_ctx = await _augmented_part_form_ctx(db) return templates.TemplateResponse( request, - "admin/_augmented_parts_sections.html", + "admin/augmented_parts_skipped.html", + { + "user": user, + **form_ctx, + "unlinked_parts": await augmented_part_crud.get_unlinked_imported_parts(db), + }, + ) + + +@router.post("/augmented-parts/skipped", response_class=HTMLResponse) +async def sync_skipped_parts( + request: Request, + db: AsyncSession = Depends(get_db), + actor: User = Depends(web_require("augmented_parts:write")), +): + form_data = await request.form() + inventory_ids = [str(v) for v in form_data.getlist("inventory_ids")] + part_type_id_str = str(form_data.get("part_type_id", "")) + sub_part_type_id_str = str(form_data.get("sub_part_type_id", "")) + + part_type_id = int(part_type_id_str) if part_type_id_str else None + sub_part_type_id = int(sub_part_type_id_str) if sub_part_type_id_str else None + + sync_result = None + sync_error = None + + if not inventory_ids: + sync_error = "No parts selected." + elif part_type_id is None: + sync_error = "No type selected." + else: + synced = 0 + errors: list[str] = [] + for inv_id in inventory_ids: + try: + ap = await augmented_part_sync_service.sync_one( + db, + inventory_id=inv_id, + part_type_id=part_type_id, + sub_part_type_id=sub_part_type_id, + ) + await audit_crud.log( + db, actor=actor, action="create", resource_type="AugmentedPart", + resource_id=ap.id, + description=audit_crud.describe_create( + "AugmentedPart", ap.id, ap.description + ), + ) + synced += 1 + except Exception as exc: + errors.append(f"{inv_id}: {exc}") + if errors: + sync_error = "Some parts failed: " + "; ".join(errors) + if synced: + sync_result = f"{synced} part(s) synced successfully." + + form_ctx = await _augmented_part_form_ctx(db) + return templates.TemplateResponse( + request, + "admin/_skipped_parts_table.html", { **form_ctx, - "augmented_parts": await augmented_part_crud.list_all(db), "unlinked_parts": await augmented_part_crud.get_unlinked_imported_parts(db), - "unlinked_sync_result": unlinked_sync_result, - "unlinked_sync_error": unlinked_sync_error, + "sync_result": sync_result, + "sync_error": sync_error, }, )