Files
HartOMat/frontend/src/api/orders.ts
T
Hartmut b892f72f7e feat: per-line render overrides — override any output type setting at order time
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>
2026-03-15 12:26:38 +01:00

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)
}