feat: initial commit
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import api from './client'
|
||||
|
||||
export interface TopLevelSummary {
|
||||
total_orders: number
|
||||
completed_orders: number
|
||||
total_revenue: number
|
||||
total_rendering_items: number
|
||||
}
|
||||
|
||||
export interface ThroughputPoint {
|
||||
week: string
|
||||
count: number
|
||||
completed: number
|
||||
}
|
||||
|
||||
export interface RevenuePoint {
|
||||
month: string
|
||||
revenue: number
|
||||
order_count: number
|
||||
}
|
||||
|
||||
export interface ProcessingTimeStats {
|
||||
avg_submit_to_complete_s: number | null
|
||||
avg_submit_to_processing_s: number | null
|
||||
p50_s: number | null
|
||||
p95_s: number | null
|
||||
}
|
||||
|
||||
export interface ItemStatusBreakdown {
|
||||
pending: number
|
||||
approved: number
|
||||
rejected: number
|
||||
}
|
||||
|
||||
export interface RenderTimeBreakdown {
|
||||
avg_stl_s: number | null
|
||||
avg_render_s: number | null
|
||||
avg_total_s: number | null
|
||||
sample_count: number
|
||||
}
|
||||
|
||||
export interface CategoryCount {
|
||||
category: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface ProductCategoryStats {
|
||||
unique_products_rendered: number
|
||||
total_products: number
|
||||
products_with_cad: number
|
||||
products_by_category: CategoryCount[]
|
||||
}
|
||||
|
||||
export interface OutputTypeUsagePoint {
|
||||
output_type: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface RenderStatusDistribution {
|
||||
pending: number
|
||||
processing: number
|
||||
completed: number
|
||||
failed: number
|
||||
}
|
||||
|
||||
export interface RendererUsagePoint {
|
||||
renderer: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface TopProductEntry {
|
||||
pim_id: string
|
||||
product_name: string | null
|
||||
category: string
|
||||
order_count: number
|
||||
}
|
||||
|
||||
export interface CategoryRevenueEntry {
|
||||
category: string
|
||||
order_count: number
|
||||
revenue: number
|
||||
}
|
||||
|
||||
export interface RenderBackendStatsEntry {
|
||||
backend: string
|
||||
total: number
|
||||
completed: number
|
||||
failed: number
|
||||
avg_render_s: number | null
|
||||
p50_render_s: number | null
|
||||
}
|
||||
|
||||
export interface RenderTimeByOutputType {
|
||||
output_type: string
|
||||
job_count: number
|
||||
avg_render_s: number | null
|
||||
min_render_s: number | null
|
||||
max_render_s: number | null
|
||||
p50_render_s: number | null
|
||||
}
|
||||
|
||||
export interface OrdersByUserEntry {
|
||||
full_name: string
|
||||
email: string
|
||||
role: string
|
||||
order_count: number
|
||||
revenue: number
|
||||
}
|
||||
|
||||
export interface DashboardKPIs {
|
||||
summary: TopLevelSummary
|
||||
throughput: ThroughputPoint[]
|
||||
revenue: RevenuePoint[]
|
||||
processing_times: ProcessingTimeStats
|
||||
item_status: ItemStatusBreakdown
|
||||
render_times: RenderTimeBreakdown
|
||||
product_stats: ProductCategoryStats
|
||||
output_type_usage: OutputTypeUsagePoint[]
|
||||
render_status: RenderStatusDistribution
|
||||
renderer_usage: RendererUsagePoint[]
|
||||
top_products: TopProductEntry[]
|
||||
orders_by_user: OrdersByUserEntry[]
|
||||
category_revenue: CategoryRevenueEntry[]
|
||||
render_backend_stats: RenderBackendStatsEntry[]
|
||||
render_time_by_output_type: RenderTimeByOutputType[]
|
||||
}
|
||||
|
||||
export async function getDashboardKPIs(
|
||||
dateFrom?: string,
|
||||
dateTo?: string,
|
||||
): Promise<DashboardKPIs> {
|
||||
const params: Record<string, string> = {}
|
||||
if (dateFrom) params.date_from = dateFrom
|
||||
if (dateTo) params.date_to = dateTo
|
||||
const res = await api.get<DashboardKPIs>('/analytics/dashboard', { params })
|
||||
return res.data
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import api from './client'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface CadObjects {
|
||||
cad_file_id: string
|
||||
original_name: string
|
||||
processing_status: 'pending' | 'processing' | 'completed' | 'failed'
|
||||
parsed_objects: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface RegenerateThumbnailResponse {
|
||||
cad_file_id: string
|
||||
original_name: string
|
||||
status: 'queued'
|
||||
task_id: string | null
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// API functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the URL to the thumbnail PNG for a CAD file.
|
||||
* Use directly in <img src={getCadThumbnailUrl(id)} /> – the browser will
|
||||
* handle the authenticated request via the axios interceptor when called
|
||||
* programmatically, or you can construct the URL for use in img tags when
|
||||
* the auth token is set as a header.
|
||||
*
|
||||
* For use in <img> tags without auth headers, prefer fetching as a blob and
|
||||
* creating an object URL (see fetchThumbnailBlob below).
|
||||
*/
|
||||
export function getCadThumbnailUrl(cadFileId: string): string {
|
||||
return `/api/cad/${cadFileId}/thumbnail`
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the thumbnail PNG as a Blob and return an object URL suitable for
|
||||
* use in <img src=...> without needing explicit auth headers.
|
||||
* Remember to call URL.revokeObjectURL() when the component unmounts.
|
||||
*/
|
||||
export async function fetchThumbnailBlob(cadFileId: string): Promise<string> {
|
||||
const res = await api.get<Blob>(`/cad/${cadFileId}/thumbnail`, {
|
||||
responseType: 'blob',
|
||||
})
|
||||
return URL.createObjectURL(res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the glTF model file as a Blob and return an object URL.
|
||||
* Remember to call URL.revokeObjectURL() when the consumer is done.
|
||||
*/
|
||||
export async function fetchModelBlob(cadFileId: string): Promise<string> {
|
||||
const res = await api.get<Blob>(`/cad/${cadFileId}/model`, {
|
||||
responseType: 'blob',
|
||||
})
|
||||
return URL.createObjectURL(res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the parsed_objects JSON for a CAD file.
|
||||
*/
|
||||
export async function getCadObjects(cadFileId: string): Promise<CadObjects> {
|
||||
const res = await api.get<CadObjects>(`/cad/${cadFileId}/objects`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the cached STL for a CAD file as a file-save dialog.
|
||||
* quality: 'low' | 'high'
|
||||
* The backend returns a human-readable filename, but we derive it client-side too.
|
||||
*/
|
||||
export async function downloadStl(cadFileId: string, quality: 'low' | 'high', suggestedName?: string): Promise<void> {
|
||||
const res = await api.get<Blob>(`/cad/${cadFileId}/stl/${quality}`, {
|
||||
responseType: 'blob',
|
||||
})
|
||||
const url = URL.createObjectURL(res.data)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = suggestedName ? `${suggestedName}_${quality}.stl` : `model_${quality}.stl`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
export async function generateStl(cadFileId: string, quality: 'low' | 'high'): Promise<{ task_id: string }> {
|
||||
const res = await api.post<{ task_id: string }>(`/cad/${cadFileId}/generate-stl/${quality}`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the backend to re-queue STEP processing for a CAD file (admin only).
|
||||
* Returns the Celery task_id (or null if the worker is not available).
|
||||
*/
|
||||
export async function regenerateThumbnail(
|
||||
cadFileId: string,
|
||||
): Promise<RegenerateThumbnailResponse> {
|
||||
const res = await api.post<RegenerateThumbnailResponse>(
|
||||
`/cad/${cadFileId}/regenerate-thumbnail`,
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import axios from 'axios'
|
||||
import { useAuthStore } from '../store/auth'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = useAuthStore.getState().token
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`
|
||||
return config
|
||||
})
|
||||
|
||||
api.interceptors.response.use(
|
||||
(res) => res,
|
||||
(err) => {
|
||||
if (err.response?.status === 401) {
|
||||
useAuthStore.getState().logout()
|
||||
window.location.href = '/login'
|
||||
}
|
||||
return Promise.reject(err)
|
||||
},
|
||||
)
|
||||
|
||||
export default api
|
||||
@@ -0,0 +1,86 @@
|
||||
import api from './client'
|
||||
|
||||
export interface Material {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
source: string
|
||||
schaeffler_code: number | null
|
||||
created_by_name: string | null
|
||||
aliases: string[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface MaterialAlias {
|
||||
id: string
|
||||
alias: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export async function listMaterials() {
|
||||
const res = await api.get<Material[]>('/materials')
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function createMaterial(data: {
|
||||
name: string
|
||||
description?: string
|
||||
source?: string
|
||||
schaeffler_code?: number | null
|
||||
}) {
|
||||
const res = await api.post<Material>('/materials', data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function updateMaterial(id: string, data: { name?: string; description?: string }) {
|
||||
const res = await api.patch<Material>(`/materials/${id}`, data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function deleteMaterial(id: string) {
|
||||
await api.delete(`/materials/${id}`)
|
||||
}
|
||||
|
||||
export async function saveCadPartMaterials(
|
||||
orderId: string,
|
||||
itemId: string,
|
||||
parts: Array<{ part_name: string; material: string }>,
|
||||
) {
|
||||
const res = await api.put(`/orders/${orderId}/items/${itemId}/cad-materials`, { parts })
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function seedSchaefflerMaterials() {
|
||||
const res = await api.post<{ inserted: number; total: number }>('/materials/seed-schaeffler')
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function getNextCode(typePrefix: string) {
|
||||
const res = await api.get<{ next_code: number; prefix: string; next_consecutive: number }>(
|
||||
`/materials/next-code`,
|
||||
{ params: { type_prefix: typePrefix } },
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
|
||||
// --- Alias endpoints ---
|
||||
|
||||
export async function listAliases(materialId: string): Promise<MaterialAlias[]> {
|
||||
const res = await api.get<MaterialAlias[]>(`/materials/${materialId}/aliases`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function addAlias(materialId: string, alias: string): Promise<MaterialAlias> {
|
||||
const res = await api.post<MaterialAlias>(`/materials/${materialId}/aliases`, { alias })
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function deleteAlias(aliasId: string): Promise<void> {
|
||||
await api.delete(`/materials/aliases/${aliasId}`)
|
||||
}
|
||||
|
||||
export async function seedAliases(): Promise<{ inserted: number; total: number }> {
|
||||
const res = await api.post<{ inserted: number; total: number }>('/materials/seed-aliases')
|
||||
return res.data
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import api from './client'
|
||||
|
||||
export interface Notification {
|
||||
id: string
|
||||
action: string
|
||||
entity_type: string | null
|
||||
entity_id: string | null
|
||||
details: Record<string, unknown> | null
|
||||
timestamp: string
|
||||
read_at: string | null
|
||||
}
|
||||
|
||||
export interface NotificationListResponse {
|
||||
items: Notification[]
|
||||
unread_count: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export async function getNotifications(params?: {
|
||||
limit?: number
|
||||
offset?: number
|
||||
unread_only?: boolean
|
||||
}): Promise<NotificationListResponse> {
|
||||
const { data } = await api.get('/notifications', { params })
|
||||
return data
|
||||
}
|
||||
|
||||
export async function getUnreadCount(): Promise<number> {
|
||||
const { data } = await api.get('/notifications/unread-count')
|
||||
return data.unread_count
|
||||
}
|
||||
|
||||
export async function markAsRead(ids?: string[]): Promise<void> {
|
||||
await api.post('/notifications/mark-read', { notification_ids: ids ?? null })
|
||||
}
|
||||
|
||||
export async function markOneAsRead(id: string): Promise<void> {
|
||||
await api.post(`/notifications/${id}/mark-read`)
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import api from './client'
|
||||
import type { Product } from './products'
|
||||
import type { OutputType } from './outputTypes'
|
||||
|
||||
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
|
||||
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
|
||||
gewuenschte_bildnummer?: string | 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
|
||||
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
|
||||
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 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 downloadOrderRenders(orderId: string, orderNumber: string): Promise<void> {
|
||||
const res = await api.get(`/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)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import api from './client'
|
||||
|
||||
export interface OutputType {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
renderer: string
|
||||
render_settings: Record<string, unknown>
|
||||
output_format: string
|
||||
sort_order: number
|
||||
compatible_categories: string[]
|
||||
render_backend: string
|
||||
is_animation: boolean
|
||||
transparent_bg: boolean
|
||||
cycles_device: string | null
|
||||
pricing_tier_id: number | null
|
||||
pricing_tier_name: string | null
|
||||
price_per_item: number | null
|
||||
is_active: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export async function listOutputTypes(
|
||||
includeInactive = false,
|
||||
category?: string,
|
||||
): Promise<OutputType[]> {
|
||||
const params: Record<string, unknown> = { include_inactive: includeInactive }
|
||||
if (category) params.category = category
|
||||
const res = await api.get<OutputType[]>('/output-types', { params })
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function createOutputType(data: Partial<OutputType>): Promise<OutputType> {
|
||||
const res = await api.post<OutputType>('/output-types', data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function updateOutputType(id: string, data: Partial<OutputType>): Promise<OutputType> {
|
||||
const res = await api.patch<OutputType>(`/output-types/${id}`, data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function deleteOutputType(id: string): Promise<void> {
|
||||
await api.delete(`/output-types/${id}`)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import api from './client'
|
||||
|
||||
export interface PricingTier {
|
||||
id: number
|
||||
category_key: string
|
||||
quality_level: string
|
||||
price_per_item: number
|
||||
description: string | null
|
||||
is_active: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export async function listPricingTiers(): Promise<PricingTier[]> {
|
||||
const res = await api.get<PricingTier[]>('/pricing')
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function createPricingTier(data: {
|
||||
category_key: string
|
||||
quality_level: string
|
||||
price_per_item: number
|
||||
description?: string
|
||||
is_active?: boolean
|
||||
}): Promise<PricingTier> {
|
||||
const res = await api.post<PricingTier>('/pricing', data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function updatePricingTier(
|
||||
id: number,
|
||||
data: { category_key?: string; quality_level?: string; price_per_item?: number; description?: string; is_active?: boolean },
|
||||
): Promise<PricingTier> {
|
||||
const res = await api.patch<PricingTier>(`/pricing/${id}`, data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function deletePricingTier(id: number): Promise<void> {
|
||||
await api.delete(`/pricing/${id}`)
|
||||
}
|
||||
|
||||
export interface PriceEstimateLine {
|
||||
product_id: string
|
||||
output_type_id: string | null
|
||||
}
|
||||
|
||||
export interface PriceEstimate {
|
||||
total: number
|
||||
line_count: number
|
||||
breakdown: Array<{
|
||||
output_type_id: string | null
|
||||
product_id: string | null
|
||||
unit_price: number | null
|
||||
}>
|
||||
has_unpriced: boolean
|
||||
}
|
||||
|
||||
export async function estimatePrice(lines: PriceEstimateLine[]): Promise<PriceEstimate> {
|
||||
const res = await api.post<PriceEstimate>('/pricing/estimate', { lines })
|
||||
return res.data
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import api from './client'
|
||||
|
||||
export interface RenderPosition {
|
||||
id: string
|
||||
product_id: string
|
||||
name: string
|
||||
rotation_x: number
|
||||
rotation_y: number
|
||||
rotation_z: number
|
||||
is_default: boolean
|
||||
sort_order: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ComponentData {
|
||||
part_name: string | null
|
||||
material: string | null
|
||||
component_type: string | null
|
||||
column_index: number
|
||||
}
|
||||
|
||||
export interface CadPartMaterial {
|
||||
part_name: string
|
||||
material: string
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: string
|
||||
pim_id: string
|
||||
name: string | null
|
||||
category_key: string | null
|
||||
ebene1: string | null
|
||||
ebene2: string | null
|
||||
baureihe: string | null
|
||||
produkt_baureihe: string | null
|
||||
lagertyp: string | null
|
||||
name_cad_modell: string | null
|
||||
gewuenschte_bildnummer: string | null
|
||||
medias_rendering: boolean | null
|
||||
components: ComponentData[]
|
||||
cad_part_materials: CadPartMaterial[]
|
||||
cad_file_id: string | null
|
||||
thumbnail_url: string | null
|
||||
render_image_url: string | null
|
||||
processing_status: string | null
|
||||
stl_cached: string[]
|
||||
cad_parsed_objects: string[] | null
|
||||
arbeitspaket: string | null
|
||||
notes: string | null
|
||||
is_active: boolean
|
||||
source_excel: string | null
|
||||
render_positions: RenderPosition[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface ProductListParams {
|
||||
q?: string
|
||||
category_key?: string
|
||||
has_cad?: boolean
|
||||
ready_only?: boolean
|
||||
materials_filter?: string // "complete" | "incomplete" | ""
|
||||
skip?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export async function listProducts(params?: ProductListParams): Promise<Product[]> {
|
||||
const res = await api.get<Product[]>('/products', { params })
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function getProduct(id: string): Promise<Product> {
|
||||
const res = await api.get<Product>(`/products/${id}`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function createProduct(data: Partial<Product>): Promise<Product> {
|
||||
const res = await api.post<Product>('/products', data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function updateProduct(id: string, data: Partial<Product>): Promise<Product> {
|
||||
const res = await api.patch<Product>(`/products/${id}`, data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function deleteProduct(id: string, hard = false): Promise<void> {
|
||||
await api.delete(`/products/${id}`, { params: hard ? { hard: true } : undefined })
|
||||
}
|
||||
|
||||
export async function uploadProductCad(id: string, file: File) {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
const res = await api.post(`/products/${id}/cad`, form, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function saveProductCadMaterials(id: string, parts: CadPartMaterial[]): Promise<Product> {
|
||||
const res = await api.post<Product>(`/products/${id}/cad-materials`, { parts })
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function regenerateProduct(id: string) {
|
||||
const res = await api.post(`/products/${id}/regenerate`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function reprocessProduct(id: string) {
|
||||
const res = await api.post(`/products/${id}/reprocess`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function reassignMaterialsFromExcel(id: string): Promise<Product> {
|
||||
const res = await api.post<Product>(`/products/${id}/reassign-materials-from-excel`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export interface ProductRender {
|
||||
order_line_id: string
|
||||
order_number: string | null
|
||||
output_type_name: string | null
|
||||
render_url: string
|
||||
is_video: boolean
|
||||
render_backend: string | null
|
||||
completed_at: string | null
|
||||
}
|
||||
|
||||
export async function getProductRenders(id: string): Promise<ProductRender[]> {
|
||||
const res = await api.get<ProductRender[]>(`/products/${id}/renders`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function deleteProductRender(productId: string, orderLineId: string): Promise<void> {
|
||||
await api.delete(`/products/${productId}/renders/${orderLineId}`)
|
||||
}
|
||||
|
||||
export async function downloadProductRenders(
|
||||
productId: string,
|
||||
orderLineIds: string[],
|
||||
filename?: string,
|
||||
): Promise<void> {
|
||||
const res = await api.post<Blob>(
|
||||
`/products/${productId}/download-renders`,
|
||||
{ order_line_ids: orderLineIds },
|
||||
{ responseType: 'blob' },
|
||||
)
|
||||
const url = URL.createObjectURL(res.data)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename ?? 'renders.zip'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
export async function getProductOrders(id: string) {
|
||||
const res = await api.get(`/products/${id}/orders`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function listRenderPositions(productId: string): Promise<RenderPosition[]> {
|
||||
const res = await api.get<RenderPosition[]>(`/products/${productId}/render-positions`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function createRenderPosition(
|
||||
productId: string,
|
||||
data: { name: string; rotation_x: number; rotation_y: number; rotation_z: number; is_default?: boolean; sort_order?: number },
|
||||
): Promise<RenderPosition> {
|
||||
const res = await api.post<RenderPosition>(`/products/${productId}/render-positions`, data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function updateRenderPosition(
|
||||
productId: string,
|
||||
posId: string,
|
||||
data: Partial<{ name: string; rotation_x: number; rotation_y: number; rotation_z: number; is_default: boolean; sort_order: number }>,
|
||||
): Promise<RenderPosition> {
|
||||
const res = await api.patch<RenderPosition>(`/products/${productId}/render-positions/${posId}`, data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function deleteRenderPosition(productId: string, posId: string): Promise<void> {
|
||||
await api.delete(`/products/${productId}/render-positions/${posId}`)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import api from './client';
|
||||
|
||||
export interface RenderTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
category_key: string | null;
|
||||
output_type_id: string | null;
|
||||
output_type_name: string | null;
|
||||
blend_file_path: string;
|
||||
original_filename: string;
|
||||
target_collection: string;
|
||||
material_replace_enabled: boolean;
|
||||
lighting_only: boolean;
|
||||
shadow_catcher_enabled: boolean;
|
||||
camera_orbit: boolean;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface MaterialLibraryInfo {
|
||||
exists: boolean;
|
||||
filename: string | null;
|
||||
size_bytes: number | null;
|
||||
path: string | null;
|
||||
}
|
||||
|
||||
export async function listRenderTemplates(): Promise<RenderTemplate[]> {
|
||||
const { data } = await api.get('/render-templates');
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function createRenderTemplate(formData: FormData): Promise<RenderTemplate> {
|
||||
const { data } = await api.post('/render-templates', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateRenderTemplate(
|
||||
id: string,
|
||||
updates: Partial<Pick<RenderTemplate, 'name' | 'category_key' | 'output_type_id' | 'target_collection' | 'material_replace_enabled' | 'lighting_only' | 'shadow_catcher_enabled' | 'camera_orbit' | 'is_active'>>,
|
||||
): Promise<RenderTemplate> {
|
||||
const { data } = await api.patch(`/render-templates/${id}`, updates);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteRenderTemplate(id: string): Promise<void> {
|
||||
await api.delete(`/render-templates/${id}`);
|
||||
}
|
||||
|
||||
export async function reuploadBlendFile(id: string, file: File): Promise<RenderTemplate> {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
const { data } = await api.post(`/render-templates/${id}/upload`, fd, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function uploadMaterialLibrary(file: File): Promise<MaterialLibraryInfo> {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
const { data } = await api.post('/admin/settings/material-library', fd, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getMaterialLibraryInfo(): Promise<MaterialLibraryInfo> {
|
||||
const { data } = await api.get('/admin/settings/material-library');
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteMaterialLibrary(): Promise<void> {
|
||||
await api.delete('/admin/settings/material-library');
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import api from './client'
|
||||
import type { OrderDetail } from './orders'
|
||||
|
||||
export interface ExcelPreviewRow {
|
||||
row_index: number
|
||||
pim_id: string | null
|
||||
produkt_baureihe: string | null
|
||||
gewaehltes_produkt: string | null
|
||||
product_exists: boolean
|
||||
product_id: string | null
|
||||
medias_rendering: boolean | null
|
||||
category_key: string | null
|
||||
has_step: boolean
|
||||
is_duplicate: boolean
|
||||
duplicate_of_row: number | null
|
||||
}
|
||||
|
||||
export interface ExcelPreviewResult {
|
||||
excel_path: string
|
||||
filename: string
|
||||
category_key: string | null
|
||||
row_count: number
|
||||
existing_product_count: number
|
||||
new_product_count: number
|
||||
no_pim_id_count: number
|
||||
has_step_count: number
|
||||
no_step_count: number
|
||||
duplicate_count: number
|
||||
warnings: string[]
|
||||
rows: ExcelPreviewRow[]
|
||||
column_headers: string[]
|
||||
template_name: string | null
|
||||
}
|
||||
|
||||
export interface ParsedComponent {
|
||||
part_name: string | null
|
||||
material: string | null
|
||||
component_type: string | null
|
||||
column_index: number
|
||||
}
|
||||
|
||||
export interface ParsedRow {
|
||||
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: ParsedComponent[]
|
||||
}
|
||||
|
||||
export interface ParsedExcelResponse {
|
||||
filename: string
|
||||
excel_path?: string
|
||||
category_key: string | null
|
||||
template_name: string | null
|
||||
row_count: number
|
||||
column_headers: string[]
|
||||
rows: ParsedRow[]
|
||||
warnings: string[]
|
||||
}
|
||||
|
||||
export interface OutputTypeSelection {
|
||||
row_index: number
|
||||
output_type_ids: string[]
|
||||
}
|
||||
|
||||
export interface ExcelFinalizeRequest {
|
||||
excel_path: string
|
||||
included_row_indices: number[]
|
||||
output_type_selections: OutputTypeSelection[]
|
||||
notes?: string | null
|
||||
template_id?: string | null
|
||||
}
|
||||
|
||||
export async function uploadExcel(file: File): Promise<ExcelPreviewResult> {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
const res = await api.post<ExcelPreviewResult>('/uploads/excel', form, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function finalizeExcelImport(data: ExcelFinalizeRequest): Promise<OrderDetail> {
|
||||
const res = await api.post<OrderDetail>('/uploads/excel/finalize', data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function uploadStep(file: File) {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
const res = await api.post('/uploads/step', form, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import api from './client'
|
||||
|
||||
export interface RenderLog {
|
||||
renderer: string
|
||||
format?: string
|
||||
engine?: string
|
||||
engine_used?: string
|
||||
samples?: number
|
||||
cycles_device?: string
|
||||
stl_quality?: string
|
||||
smooth_angle?: number
|
||||
width?: number
|
||||
height?: number
|
||||
total_duration_s?: number
|
||||
stl_duration_s?: number
|
||||
render_duration_s?: number
|
||||
stl_size_bytes?: number
|
||||
output_size_bytes?: number
|
||||
parts_count?: number
|
||||
log_lines?: string[]
|
||||
fallback?: boolean
|
||||
started_at?: string
|
||||
completed_at?: string
|
||||
}
|
||||
|
||||
export interface CadActivityEntry {
|
||||
cad_file_id: string
|
||||
original_name: string
|
||||
file_size: number | null
|
||||
processing_status: 'pending' | 'processing' | 'completed' | 'failed' | string
|
||||
error_message: string | null
|
||||
updated_at: string
|
||||
created_at: string
|
||||
order_numbers: string[]
|
||||
render_log: RenderLog | null
|
||||
}
|
||||
|
||||
export interface RenderJobEntry {
|
||||
order_line_id: string
|
||||
order_number: string | null
|
||||
product_name: string | null
|
||||
output_type_name: string | null
|
||||
render_status: 'processing' | 'completed' | 'failed' | string
|
||||
render_backend_used: string | null
|
||||
flamenco_job_id: string | null
|
||||
render_started_at: string | null
|
||||
render_completed_at: string | null
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface WorkerActivity {
|
||||
cad_processing: CadActivityEntry[]
|
||||
active_count: number
|
||||
failed_count: number
|
||||
render_jobs: RenderJobEntry[]
|
||||
render_active_count: number
|
||||
render_failed_count: number
|
||||
}
|
||||
|
||||
export async function getWorkerActivity(): Promise<WorkerActivity> {
|
||||
const res = await api.get<WorkerActivity>('/worker/activity')
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function reprocessCadFile(cad_file_id: string): Promise<void> {
|
||||
await api.post(`/worker/activity/${cad_file_id}/reprocess`)
|
||||
}
|
||||
|
||||
export interface RenderLogEntry {
|
||||
ts: number
|
||||
t: string
|
||||
level: 'info' | 'error' | 'success' | string
|
||||
msg: string
|
||||
}
|
||||
|
||||
export async function getRenderLog(orderLineId: string, after: number = 0): Promise<{
|
||||
entries: RenderLogEntry[]
|
||||
total: number
|
||||
next_after: number
|
||||
}> {
|
||||
const res = await api.get(`/worker/render-log/${orderLineId}`, { params: { after } })
|
||||
return res.data
|
||||
}
|
||||
|
||||
/** Returns the SSE URL for streaming render logs (needs token in query param). */
|
||||
export function renderLogStreamUrl(orderLineId: string): string {
|
||||
return `/api/worker/render-log/${orderLineId}/stream`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Queue inspection + control
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface QueueTask {
|
||||
task_id: string
|
||||
task_name: string
|
||||
args: any[]
|
||||
argsrepr: string
|
||||
status: 'pending' | 'active' | 'reserved'
|
||||
worker?: string
|
||||
queue?: string
|
||||
}
|
||||
|
||||
export interface QueueStatus {
|
||||
queue_depths: Record<string, number>
|
||||
pending_count: number
|
||||
active: QueueTask[]
|
||||
reserved: QueueTask[]
|
||||
pending: QueueTask[]
|
||||
}
|
||||
|
||||
export async function getQueueStatus(): Promise<QueueStatus> {
|
||||
const res = await api.get<QueueStatus>('/worker/queue')
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function purgeQueue(): Promise<{ purged: number; message: string }> {
|
||||
const res = await api.post<{ purged: number; message: string }>('/worker/queue/purge')
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function cancelTask(taskId: string): Promise<{ revoked: string }> {
|
||||
const res = await api.post<{ revoked: string }>(`/worker/queue/cancel/${taskId}`)
|
||||
return res.data
|
||||
}
|
||||
Reference in New Issue
Block a user