feat: batch material override — apply to all lines in an order at once

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 14:36:18 +01:00
parent 9d6def84c1
commit d84ce8252e
3 changed files with 71 additions and 1 deletions
+26
View File
@@ -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
+8
View File
@@ -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}`,
+37 -1
View File
@@ -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() {
</div>
)}
{(order.lines?.length ?? 0) > 0 && isPrivileged && (
<div className="flex items-center gap-2 mb-2 px-1">
<span className="text-xs text-content-muted">Batch material override:</span>
<select
className="text-xs border border-border-default rounded px-2 py-1"
style={{ backgroundColor: 'var(--color-bg-surface)' }}
value=""
onChange={(e) => {
const val = e.target.value
if (val === '__clear__') batchOverrideMut.mutate(null)
else if (val) batchOverrideMut.mutate(val)
}}
disabled={batchOverrideMut.isPending}
>
<option value="">Apply to all lines</option>
<option value="__clear__"> Clear all overrides </option>
{orderLibMats.map((m: Material) => (
<option key={m.id} value={m.name}>{m.name.replace('SCHAEFFLER_', '').replace(/_/g, ' ')}</option>
))}
</select>
{batchOverrideMut.isPending && <Loader2 size={12} className="animate-spin text-accent" />}
</div>
)}
{(order.lines?.length ?? 0) > 0 && (
<div className="overflow-auto">
<table className="w-full text-sm">