f19a6ccde8
Phase F — STL Hash Cache:
- Migration 041: step_file_hash column on cad_files
- cache_service.py: SHA256 hash + MinIO-backed STL cache (check/store)
- render_step_thumbnail: compute+persist hash before render
- generate_stl_cache: check MinIO cache before cadquery conversion, store after
Phase G — Invoices:
- Migration 042: invoices + invoice_lines tables with RLS
- Invoice/InvoiceLine models + schemas
- billing service: generate_invoice_number (INV-YYYY-NNNN), create/list/get/delete/PDF
- WeasyPrint PDF generation; backend Dockerfile + pyproject.toml deps
- invoice_router with 6 endpoints; registered in main.py
- frontend: Billing.tsx page + api/billing.ts; route + nav link
Phase H — Import Sanity Check:
- Migration 043: import_validations table
- ImportValidation model + schemas
- run_sanity_check: material fuzzy-match (cutoff=0.8), STEP availability, duplicate detection
- validate_excel_import Celery task (queue: step_processing)
- uploads.py: create ImportValidation on /excel, fire task, expose GET /validations/{id}
- frontend: Upload.tsx polling ValidationDialog with Ampel status indicators
Phase I — Notification Settings:
- Migration 044: notification_configs table (user×event×channel toggles)
- NotificationConfig model + seeds (in_app=true, email=false)
- get/upsert/reset config endpoints on /notifications/config
- frontend: NotificationSettings.tsx page + api/notifications.ts extensions
Infrastructure:
- docker-compose.yml: add worker-thumbnail service (concurrency=1, Q=thumbnail_rendering)
- Fix Dockerfile: libgdk-pixbuf-2.0-0 (correct Debian bookworm package name)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
147 lines
3.7 KiB
TypeScript
147 lines
3.7 KiB
TypeScript
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
|
|
validation_id?: 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
|
|
}
|
|
|
|
// ── Import Validation ─────────────────────────────────────────────────────
|
|
|
|
export interface ValidationIssue {
|
|
type: 'missing_material' | 'material_suggestion' | 'no_step' | 'duplicate'
|
|
field: string | null
|
|
value: string | null
|
|
suggestion: string | null
|
|
message: string
|
|
}
|
|
|
|
export interface ValidationRow {
|
|
row_index: number
|
|
product_id: string | null
|
|
pim_id: string | null
|
|
produkt_baureihe: string | null
|
|
issues: ValidationIssue[]
|
|
status: 'ok' | 'warning' | 'error'
|
|
}
|
|
|
|
export interface ImportValidation {
|
|
id: string
|
|
tenant_id: string | null
|
|
excel_path: string
|
|
status: 'pending' | 'running' | 'completed' | 'failed'
|
|
summary: {
|
|
total: number
|
|
ok: number
|
|
warnings: number
|
|
errors: number
|
|
missing_materials: number
|
|
no_step: number
|
|
duplicates: number
|
|
} | null
|
|
rows: ValidationRow[] | null
|
|
created_at: string
|
|
completed_at: string | null
|
|
}
|
|
|
|
export async function getImportValidation(id: string): Promise<ImportValidation> {
|
|
const res = await api.get<ImportValidation>(`/uploads/validations/${id}`)
|
|
return res.data
|
|
}
|