b892f72f7e
Instead of duplicating output types for every variation (WebP vs PNG,
different resolution), keep one canonical output type and override
specific fields per order line via render_overrides JSONB.
Backend:
- render_overrides JSONB column on OrderLine (DB migration)
- Render task merges overrides with output type settings (format, width,
height, samples, engine, denoiser, transparent_bg, cycles_device)
- POST /orders/{id}/batch-render-overrides endpoint for bulk override
- PatchLineBody accepts render_overrides for per-line patching
Frontend:
- Batch render overrides section on OrderDetail: output format dropdown
(PNG/JPG/WebP) + resolution dropdown (512-4096)
- Clear button to remove overrides
MCP:
- create_order tool: accepts product_ids, output_type, render_overrides,
material_override — enables "render all products as WebP" via Claude
- set_render_overrides tool: batch override on existing orders
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
350 lines
10 KiB
TypeScript
350 lines
10 KiB
TypeScript
import api from './client'
|
|
import type { Product } from './products'
|
|
import type { OutputType } from './outputTypes'
|
|
|
|
export interface RenderLog {
|
|
renderer?: string
|
|
type?: string
|
|
format?: string
|
|
engine?: string
|
|
engine_used?: string
|
|
samples?: number
|
|
smooth_angle?: number
|
|
total_duration_s?: number
|
|
stl_duration_s?: number
|
|
render_duration_s?: number
|
|
ffmpeg_duration_s?: number
|
|
stl_size_bytes?: number
|
|
output_size_bytes?: number
|
|
parts_count?: number
|
|
device_used?: string
|
|
compute_type?: string
|
|
gpu_fallback?: boolean
|
|
frame_count?: number
|
|
fps?: number
|
|
template?: string
|
|
lighting_only?: boolean
|
|
shadow_catcher?: boolean
|
|
material_replace?: boolean
|
|
fallback?: boolean
|
|
error?: string
|
|
started_at?: string
|
|
completed_at?: string
|
|
log_lines?: string[]
|
|
}
|
|
|
|
export interface OrderLine {
|
|
id: string
|
|
order_id: string
|
|
product_id: string
|
|
product: Product
|
|
output_type_id: string | null
|
|
output_type: OutputType | null
|
|
gewuenschte_bildnummer: string | null
|
|
item_status: 'pending' | 'approved' | 'rejected'
|
|
render_status: 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled'
|
|
result_path: string | null
|
|
thumbnail_url: string | null
|
|
ai_validation_status: string
|
|
ai_validation_result: Record<string, unknown> | null
|
|
render_backend_used: string | null
|
|
flamenco_job_id: string | null
|
|
unit_price: number | null
|
|
render_position_id: string | null
|
|
render_position_name: string | null
|
|
render_log: RenderLog | null
|
|
render_started_at: string | null
|
|
render_completed_at: string | null
|
|
material_override: string | null
|
|
render_overrides: Record<string, unknown> | null
|
|
notes: string | null
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
export interface OrderLineCreate {
|
|
product_id: string
|
|
output_type_id?: string | null
|
|
render_position_id?: string | null
|
|
global_render_position_id?: string | null
|
|
gewuenschte_bildnummer?: string | null
|
|
material_override?: string | null
|
|
render_overrides?: Record<string, unknown> | null
|
|
notes?: string | null
|
|
}
|
|
|
|
export interface Order {
|
|
id: string
|
|
order_number: string
|
|
template_id: string | null
|
|
status: 'draft' | 'submitted' | 'processing' | 'completed' | 'rejected'
|
|
created_by: string
|
|
source_excel: string | null
|
|
notes: string | null
|
|
created_at: string
|
|
updated_at: string
|
|
submitted_at: string | null
|
|
completed_at: string | null
|
|
rejected_at: string | null
|
|
rejection_reason: string | null
|
|
estimated_price: number | null
|
|
item_count: number
|
|
line_count: number
|
|
render_progress: {
|
|
total: number
|
|
completed: number
|
|
processing: number
|
|
failed: number
|
|
pending: number
|
|
cancelled: number
|
|
} | null
|
|
}
|
|
|
|
export interface OrderItem {
|
|
id: string
|
|
order_id: string
|
|
row_index: number
|
|
ebene1: string | null
|
|
ebene2: string | null
|
|
baureihe: string | null
|
|
pim_id: string | null
|
|
produkt_baureihe: string | null
|
|
gewaehltes_produkt: string | null
|
|
name_cad_modell: string | null
|
|
gewuenschte_bildnummer: string | null
|
|
lagertyp: string | null
|
|
medias_rendering: boolean | null
|
|
components: Array<{ part_name: string | null; material: string | null; component_type: string | null; column_index: number }>
|
|
cad_file_id: string | null
|
|
thumbnail_path: string | null
|
|
ai_validation_status: string
|
|
ai_validation_result: Record<string, unknown> | null
|
|
cad_parsed_objects: string[] | null
|
|
cad_part_materials: Array<{ part_name: string; material: string }>
|
|
item_status: 'pending' | 'approved' | 'rejected'
|
|
notes: string | null
|
|
created_at: string
|
|
}
|
|
|
|
export interface OrderDetail extends Order {
|
|
items: OrderItem[]
|
|
lines: OrderLine[]
|
|
}
|
|
|
|
export async function listOrders(params?: { status?: string; skip?: number; limit?: number }) {
|
|
const res = await api.get<Order[]>('/orders', { params })
|
|
return res.data
|
|
}
|
|
|
|
export async function searchOrders(params: {
|
|
q?: string
|
|
statuses?: string[]
|
|
date_from?: string
|
|
date_to?: string
|
|
limit?: number
|
|
}): Promise<OrderDetail[]> {
|
|
const res = await api.get<OrderDetail[]>('/orders/search', {
|
|
params: {
|
|
q: params.q || '',
|
|
statuses: params.statuses?.join(',') || '',
|
|
date_from: params.date_from || '',
|
|
date_to: params.date_to || '',
|
|
limit: params.limit || 50,
|
|
},
|
|
})
|
|
return res.data
|
|
}
|
|
|
|
export async function getOrder(id: string) {
|
|
const res = await api.get<OrderDetail>(`/orders/${id}`)
|
|
return res.data
|
|
}
|
|
|
|
export async function patchOrderItem(
|
|
orderId: string,
|
|
itemId: string,
|
|
patch: Partial<{
|
|
ebene1: string | null
|
|
ebene2: string | null
|
|
baureihe: string | null
|
|
pim_id: string | null
|
|
produkt_baureihe: string | null
|
|
gewaehltes_produkt: string | null
|
|
name_cad_modell: string | null
|
|
gewuenschte_bildnummer: string | null
|
|
lagertyp: string | null
|
|
medias_rendering: boolean | null
|
|
notes: string | null
|
|
}>,
|
|
) {
|
|
const res = await api.patch<OrderItem>(`/orders/${orderId}/items/${itemId}`, patch)
|
|
return res.data
|
|
}
|
|
|
|
export async function createOrder(data: {
|
|
template_id?: string
|
|
source_excel?: string
|
|
notes?: string
|
|
items?: Array<{
|
|
row_index: number
|
|
ebene1?: string | null
|
|
ebene2?: string | null
|
|
baureihe?: string | null
|
|
pim_id?: string | null
|
|
produkt_baureihe?: string | null
|
|
gewaehltes_produkt?: string | null
|
|
name_cad_modell?: string | null
|
|
gewuenschte_bildnummer?: string | null
|
|
lagertyp?: string | null
|
|
medias_rendering?: boolean | null
|
|
components: Array<{ part_name?: string | null; material?: string | null; component_type?: string | null; column_index: number }>
|
|
}>
|
|
lines?: OrderLineCreate[]
|
|
}) {
|
|
const res = await api.post<OrderDetail>('/orders', data)
|
|
return res.data
|
|
}
|
|
|
|
export async function addOrderLine(orderId: string, data: OrderLineCreate): Promise<OrderLine> {
|
|
const res = await api.post<OrderLine>(`/orders/${orderId}/lines`, data)
|
|
return res.data
|
|
}
|
|
|
|
export async function removeOrderLine(orderId: string, lineId: string): Promise<void> {
|
|
await api.delete(`/orders/${orderId}/lines/${lineId}`)
|
|
}
|
|
|
|
export async function submitOrder(id: string) {
|
|
const res = await api.post<Order>(`/orders/${id}/submit`)
|
|
return res.data
|
|
}
|
|
|
|
export async function deleteOrder(id: string) {
|
|
await api.delete(`/orders/${id}`)
|
|
}
|
|
|
|
export async function unlinkCadFile(orderId: string, itemId: string) {
|
|
await api.delete(`/orders/${orderId}/items/${itemId}/cad-file`)
|
|
}
|
|
|
|
export async function dispatchRenders(orderId: string) {
|
|
const res = await api.post<{ dispatched: number }>(`/orders/${orderId}/dispatch-renders`)
|
|
return res.data
|
|
}
|
|
|
|
export async function cancelLineRender(orderId: string, lineId: string) {
|
|
const res = await api.post<{ cancelled: boolean; line_id: string; backend: string; errors: string[] | null }>(
|
|
`/orders/${orderId}/lines/${lineId}/cancel-render`
|
|
)
|
|
return res.data
|
|
}
|
|
|
|
export async function dispatchLineRender(orderId: string, lineId: string) {
|
|
const res = await api.post<{ dispatched: boolean; line_id: string }>(
|
|
`/orders/${orderId}/lines/${lineId}/dispatch-render`
|
|
)
|
|
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; render_overrides?: Record<string, unknown> | null }) {
|
|
const res = await api.patch<{ updated: boolean; line_id: string }>(
|
|
`/orders/${orderId}/lines/${lineId}`,
|
|
data
|
|
)
|
|
return res.data
|
|
}
|
|
|
|
export async function batchRenderOverrides(orderId: string, renderOverrides: Record<string, unknown> | null) {
|
|
const res = await api.post<{ updated: number; render_overrides: Record<string, unknown> | null }>(
|
|
`/orders/${orderId}/batch-render-overrides`,
|
|
{ render_overrides: renderOverrides }
|
|
)
|
|
return res.data
|
|
}
|
|
|
|
export async function cancelOrderRenders(orderId: string) {
|
|
const res = await api.post<{ cancelled: number; order_status: string; errors: string[] | null }>(
|
|
`/orders/${orderId}/cancel-renders`
|
|
)
|
|
return res.data
|
|
}
|
|
|
|
export async function regenerateItemThumbnail(orderId: string, itemId: string) {
|
|
const res = await api.post<{ status: string; task_id: string; cad_file_id: string }>(
|
|
`/orders/${orderId}/items/${itemId}/regenerate-thumbnail`
|
|
)
|
|
return res.data
|
|
}
|
|
|
|
export interface SplitMissingStepResult {
|
|
new_order_id: string
|
|
new_order_number: string
|
|
moved_item_count: number
|
|
moved_line_count: number
|
|
}
|
|
|
|
export async function splitMissingStep(orderId: string): Promise<SplitMissingStepResult> {
|
|
const res = await api.post<SplitMissingStepResult>(`/orders/${orderId}/split-missing-step`)
|
|
return res.data
|
|
}
|
|
|
|
export interface GenerateLinesResult {
|
|
created: number
|
|
skipped: number
|
|
no_product_count: number
|
|
no_step_count: number
|
|
}
|
|
|
|
export async function generateLinesFromItems(
|
|
orderId: string,
|
|
outputTypeIds: string[],
|
|
): Promise<GenerateLinesResult> {
|
|
const res = await api.post<GenerateLinesResult>(`/orders/${orderId}/generate-lines`, {
|
|
output_type_ids: outputTypeIds,
|
|
})
|
|
return res.data
|
|
}
|
|
|
|
export async function rejectOrderLine(
|
|
orderId: string,
|
|
lineId: string,
|
|
reason: string,
|
|
): Promise<{ rejected: boolean; line_id: string; reason: string }> {
|
|
const res = await api.post<{ rejected: boolean; line_id: string; reason: string }>(
|
|
`/orders/${orderId}/lines/${lineId}/reject`,
|
|
{ reason },
|
|
)
|
|
return res.data
|
|
}
|
|
|
|
export async function rejectOrder(orderId: string, reason: string, notifyClient: boolean = true): Promise<Order> {
|
|
const res = await api.post<Order>(`/orders/${orderId}/reject`, {
|
|
reason,
|
|
notify_client: notifyClient,
|
|
})
|
|
return res.data
|
|
}
|
|
|
|
export async function resubmitOrder(orderId: string): Promise<Order> {
|
|
const res = await api.post<Order>(`/orders/${orderId}/resubmit`)
|
|
return res.data
|
|
}
|
|
|
|
export async function downloadOrderRenders(orderId: string, orderNumber: string): Promise<void> {
|
|
const res = await api.get<Blob>(`/orders/${orderId}/download-renders`, { responseType: 'blob' })
|
|
const url = URL.createObjectURL(res.data)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = `${orderNumber}_renders.zip`
|
|
a.click()
|
|
URL.revokeObjectURL(url)
|
|
}
|