From d84ce8252ec7c943f5fbe92f8aede30367a26df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Sat, 14 Mar 2026 14:36:18 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20batch=20material=20override=20=E2=80=94?= =?UTF-8?q?=20apply=20to=20all=20lines=20in=20an=20order=20at=20once?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /orders/{id}/batch-material-override endpoint - Dropdown above the lines table: "Apply to all lines…" - Options: clear all overrides, or select a library material - Updates all order lines in one request Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/api/routers/orders.py | 26 ++++++++++++++++++++ frontend/src/api/orders.ts | 8 +++++++ frontend/src/pages/OrderDetail.tsx | 38 +++++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/backend/app/api/routers/orders.py b/backend/app/api/routers/orders.py index e39cac8..e3011c8 100644 --- a/backend/app/api/routers/orders.py +++ b/backend/app/api/routers/orders.py @@ -1097,6 +1097,32 @@ async def dispatch_single_line_render( return {"dispatched": True, "line_id": str(line.id)} +class BatchMaterialOverrideBody(BaseModel): + material_override: str | None = None + + +@router.post("/{order_id}/batch-material-override") +async def batch_material_override( + order_id: uuid.UUID, + body: BatchMaterialOverrideBody, + user: User = Depends(require_admin_or_pm), + db: AsyncSession = Depends(get_db), +): + """Set material_override on ALL lines of an order at once.""" + result = await db.execute(select(Order).where(Order.id == order_id)) + if not result.scalar_one_or_none(): + raise HTTPException(404, detail="Order not found") + + from sqlalchemy import update as sql_update + res = await db.execute( + sql_update(OrderLine) + .where(OrderLine.order_id == order_id) + .values(material_override=body.material_override) + ) + await db.commit() + return {"updated": res.rowcount, "material_override": body.material_override} + + class PatchLineBody(BaseModel): material_override: str | None = None diff --git a/frontend/src/api/orders.ts b/frontend/src/api/orders.ts index fb83dcd..5271422 100644 --- a/frontend/src/api/orders.ts +++ b/frontend/src/api/orders.ts @@ -244,6 +244,14 @@ export async function dispatchLineRender(orderId: string, lineId: string) { return res.data } +export async function batchMaterialOverride(orderId: string, materialOverride: string | null) { + const res = await api.post<{ updated: number; material_override: string | null }>( + `/orders/${orderId}/batch-material-override`, + { material_override: materialOverride } + ) + return res.data +} + export async function patchOrderLine(orderId: string, lineId: string, data: { material_override?: string | null }) { const res = await api.patch<{ updated: boolean; line_id: string }>( `/orders/${orderId}/lines/${lineId}`, diff --git a/frontend/src/pages/OrderDetail.tsx b/frontend/src/pages/OrderDetail.tsx index b044ae1..7447e50 100644 --- a/frontend/src/pages/OrderDetail.tsx +++ b/frontend/src/pages/OrderDetail.tsx @@ -12,7 +12,7 @@ import { XCircle, RotateCw, Info, } from 'lucide-react' import { toast } from 'sonner' -import { getOrder, submitOrder, deleteOrder, unlinkCadFile, regenerateItemThumbnail, patchOrderItem, removeOrderLine, dispatchRenders, cancelLineRender, dispatchLineRender, cancelOrderRenders, splitMissingStep, generateLinesFromItems, downloadOrderRenders, rejectOrder, resubmitOrder, rejectOrderLine, patchOrderLine } from '../api/orders' +import { getOrder, submitOrder, deleteOrder, unlinkCadFile, regenerateItemThumbnail, patchOrderItem, removeOrderLine, dispatchRenders, cancelLineRender, dispatchLineRender, cancelOrderRenders, splitMissingStep, generateLinesFromItems, downloadOrderRenders, rejectOrder, resubmitOrder, rejectOrderLine, patchOrderLine, batchMaterialOverride } from '../api/orders' import { checkOrderMaterials, listMaterials, type UnmappedMaterial, type Material } from '../api/materials' import UnmappedMaterialsDialog from '../components/orders/UnmappedMaterialsDialog' import type { OrderItem, OrderLine } from '../api/orders' @@ -128,6 +128,18 @@ export default function OrderDetailPage() { } } + const { data: matList } = useQuery({ queryKey: ['materials'], queryFn: listMaterials }) + const orderLibMats = (matList ?? []).filter((m: Material) => m.schaeffler_code !== null).sort((a: Material, b: Material) => a.name.localeCompare(b.name)) + + const batchOverrideMut = useMutation({ + mutationFn: (val: string | null) => batchMaterialOverride(id!, val), + onSuccess: (data) => { + toast.success(`Material override ${data.material_override ? 'set' : 'cleared'} on ${data.updated} lines`) + qc.invalidateQueries({ queryKey: ['order', id] }) + }, + onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed'), + }) + const cancelAllMut = useMutation({ mutationFn: () => cancelOrderRenders(id!), onSuccess: (data) => { @@ -606,6 +618,30 @@ export default function OrderDetailPage() { )} + {(order.lines?.length ?? 0) > 0 && isPrivileged && ( +
+ Batch material override: + + {batchOverrideMut.isPending && } +
+ )} + {(order.lines?.length ?? 0) > 0 && (