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:
@@ -1097,6 +1097,32 @@ async def dispatch_single_line_render(
|
|||||||
return {"dispatched": True, "line_id": str(line.id)}
|
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):
|
class PatchLineBody(BaseModel):
|
||||||
material_override: str | None = None
|
material_override: str | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -244,6 +244,14 @@ export async function dispatchLineRender(orderId: string, lineId: string) {
|
|||||||
return res.data
|
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 }) {
|
export async function patchOrderLine(orderId: string, lineId: string, data: { material_override?: string | null }) {
|
||||||
const res = await api.patch<{ updated: boolean; line_id: string }>(
|
const res = await api.patch<{ updated: boolean; line_id: string }>(
|
||||||
`/orders/${orderId}/lines/${lineId}`,
|
`/orders/${orderId}/lines/${lineId}`,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
XCircle, RotateCw, Info,
|
XCircle, RotateCw, Info,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
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 { checkOrderMaterials, listMaterials, type UnmappedMaterial, type Material } from '../api/materials'
|
||||||
import UnmappedMaterialsDialog from '../components/orders/UnmappedMaterialsDialog'
|
import UnmappedMaterialsDialog from '../components/orders/UnmappedMaterialsDialog'
|
||||||
import type { OrderItem, OrderLine } from '../api/orders'
|
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({
|
const cancelAllMut = useMutation({
|
||||||
mutationFn: () => cancelOrderRenders(id!),
|
mutationFn: () => cancelOrderRenders(id!),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -606,6 +618,30 @@ export default function OrderDetailPage() {
|
|||||||
</div>
|
</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 && (
|
{(order.lines?.length ?? 0) > 0 && (
|
||||||
<div className="overflow-auto">
|
<div className="overflow-auto">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
|
|||||||
Reference in New Issue
Block a user