b583b0d7a2
- Per-render-position focal_length_mm/sensor_width_mm (DB → pipeline → Blender)
- FOV-based camera distance with min clamp fix for wide-angle lenses
- Unmapped materials blocking dialog on "Dispatch Renders" with batch alias creation
- Material check endpoint (GET /orders/{id}/check-materials)
- Batch alias endpoint (POST /materials/batch-aliases)
- Quick-map "No alias" badges on Materials page
- Full product hard-delete with storage cleanup (MinIO + disk files + orphaned CadFile)
- Delete button on ProductDetail page with confirmation
- Clickable product names in Media Browser (links to product page)
- Single-line render dispatch/retry (POST /orders/{id}/lines/{id}/dispatch-render)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
192 lines
9.5 KiB
Markdown
192 lines
9.5 KiB
Markdown
# Plan: Material Alias Completeness with Blocking Dialog
|
|
|
|
## Context
|
|
|
|
The material system currently has three resolution tiers in `resolve_material_map()`:
|
|
1. **Alias lookup** (case-insensitive) — maps raw names to SCHAEFFLER library materials
|
|
2. **Exact Material.name match** — the raw name IS a library material
|
|
3. **Pass-through** — unresolved names fall through, causing magenta "FailedMaterial" in Blender renders
|
|
|
|
Materials are stored on `Product.cad_part_materials` as `[{part_name, material}]` JSONB. They are auto-populated from Excel components during STEP processing (`_auto_populate_materials_for_cad`). The Materials page shows "Custom" materials (no `schaeffler_code`) separately from categorized SCHAEFFLER library materials.
|
|
|
|
**Problem**: When a product has a material name like "Steel--Stahl" that has no alias pointing to a SCHAEFFLER library material, the render silently produces magenta parts. There is no pre-flight check before dispatching renders.
|
|
|
|
**Solution**: Add a blocking dialog at "Dispatch Renders" that checks for unmapped materials, lets the user map them inline to library materials, and only proceeds when all materials are resolved.
|
|
|
|
### Architecture
|
|
|
|
```
|
|
Frontend (OrderDetail.tsx)
|
|
|
|
|
|-- Click "Dispatch Renders"
|
|
|-- Call GET /api/orders/{id}/check-materials (new endpoint)
|
|
|-- If unmapped materials exist:
|
|
| |-- Show UnmappedMaterialsDialog (new component)
|
|
| |-- User maps each material via dropdown
|
|
| |-- Call POST /api/materials/batch-aliases (new endpoint)
|
|
| |-- On success, proceed with dispatchRenders()
|
|
|-- If all mapped: proceed directly
|
|
```
|
|
|
|
## Affected Files
|
|
|
|
| File | Change |
|
|
|------|--------|
|
|
| `backend/app/services/material_service.py` | Add `find_unmapped_materials()` function |
|
|
| `backend/app/api/routers/orders.py` | Add `GET /orders/{id}/check-materials` endpoint |
|
|
| `backend/app/api/routers/materials.py` | Add `POST /materials/batch-aliases` endpoint |
|
|
| `frontend/src/api/materials.ts` | Add `checkOrderMaterials()`, `batchCreateAliases()` API functions + types |
|
|
| `frontend/src/components/orders/UnmappedMaterialsDialog.tsx` | New blocking dialog component |
|
|
| `frontend/src/pages/OrderDetail.tsx` | Intercept "Dispatch Renders" with material check |
|
|
| `frontend/src/pages/Materials.tsx` | Add warning badges for unmapped custom materials |
|
|
|
|
## Tasks (in order)
|
|
|
|
### [x] Task 1: Backend — `find_unmapped_materials()` service function
|
|
|
|
- **File**: `backend/app/services/material_service.py`
|
|
- **What**: Add an async function `find_unmapped_materials(material_names: list[str], db: AsyncSession) -> list[dict]` that:
|
|
1. Takes a list of raw material name strings
|
|
2. Loads all `MaterialAlias` records and all `Material` records with `schaeffler_code`
|
|
3. For each raw name, checks: (a) alias match (case-insensitive), (b) exact `Material.name` match where it has a `schaeffler_code` (i.e. it IS a library material)
|
|
4. Returns a list of `{"raw_name": str, "suggestions": [{"id": str, "name": str, "schaeffler_code": str}]}` for each unmapped name
|
|
5. `suggestions`: top 5 SCHAEFFLER materials by `difflib.SequenceMatcher` similarity (ratio > 0.3)
|
|
- **Acceptance gate**: Returns empty list when all materials are mapped; returns unmapped entries with suggestions when some are not
|
|
- **Dependencies**: None
|
|
- **Risk**: None
|
|
|
|
### [x] Task 2: Backend — `GET /orders/{id}/check-materials` endpoint
|
|
|
|
- **File**: `backend/app/api/routers/orders.py`
|
|
- **What**: Add endpoint that:
|
|
1. Loads all `OrderLine` records for the order, joining `Product`
|
|
2. Collects all unique material names from `product.cad_part_materials[*].material` across all products
|
|
3. Calls `find_unmapped_materials()` with those names
|
|
4. Returns `{"unmapped": [...], "total_materials": int, "mapped_count": int}`
|
|
- **Response schema**:
|
|
```python
|
|
class MaterialSuggestion(BaseModel):
|
|
id: uuid.UUID
|
|
name: str
|
|
schaeffler_code: str
|
|
|
|
class UnmappedMaterial(BaseModel):
|
|
raw_name: str
|
|
suggestions: list[MaterialSuggestion]
|
|
|
|
class UnmappedMaterialCheck(BaseModel):
|
|
unmapped: list[UnmappedMaterial]
|
|
total_materials: int
|
|
mapped_count: int
|
|
```
|
|
- **Auth**: `get_current_user` (any authenticated user can check)
|
|
- **Acceptance gate**: Returns correct unmapped materials for an order with mixed mapped/unmapped materials
|
|
- **Dependencies**: Task 1
|
|
- **Risk**: None
|
|
|
|
### [x] Task 3: Backend — `POST /materials/batch-aliases` endpoint
|
|
|
|
- **File**: `backend/app/api/routers/materials.py`
|
|
- **What**: Add batch alias creation endpoint:
|
|
1. Accepts `{"mappings": [{"alias": str, "material_id": uuid}]}`
|
|
2. For each mapping, creates a `MaterialAlias` record (skips if alias already exists, case-insensitive)
|
|
3. Returns `{"created": int, "skipped": int}`
|
|
- **Auth**: `require_admin_or_pm` — only admins/PMs should be able to create aliases
|
|
- **Validation**: Verify each `material_id` exists; reject if alias is empty; skip duplicate aliases
|
|
- **Acceptance gate**: Batch creates multiple aliases in one request; subsequent `resolve_material_map()` calls resolve through new aliases
|
|
- **Dependencies**: None
|
|
- **Risk**: None
|
|
|
|
### [x] Task 4: Frontend — API functions for material checking and batch alias creation
|
|
|
|
- **File**: `frontend/src/api/materials.ts`
|
|
- **What**: Add TypeScript interfaces and API functions:
|
|
```typescript
|
|
export interface MaterialSuggestion {
|
|
id: string
|
|
name: string
|
|
schaeffler_code: string
|
|
}
|
|
|
|
export interface UnmappedMaterial {
|
|
raw_name: string
|
|
suggestions: MaterialSuggestion[]
|
|
}
|
|
|
|
export interface UnmappedMaterialCheck {
|
|
unmapped: UnmappedMaterial[]
|
|
total_materials: number
|
|
mapped_count: number
|
|
}
|
|
|
|
export async function checkOrderMaterials(orderId: string): Promise<UnmappedMaterialCheck>
|
|
export async function batchCreateAliases(mappings: Array<{ alias: string; material_id: string }>): Promise<{ created: number; skipped: number }>
|
|
```
|
|
- **Acceptance gate**: Types compile; functions callable from components
|
|
- **Dependencies**: Tasks 2, 3
|
|
- **Risk**: None
|
|
|
|
### [x] Task 5: Frontend — `UnmappedMaterialsDialog` component
|
|
|
|
- **File**: `frontend/src/components/orders/UnmappedMaterialsDialog.tsx`
|
|
- **What**: Create a modal dialog component:
|
|
1. **Props**: `open: boolean`, `unmapped: UnmappedMaterial[]`, `onResolved: () => void`, `onCancel: () => void`
|
|
2. **UI**: Fixed overlay (same pattern as existing modals), wider (`max-w-xl`):
|
|
- Warning icon (`AlertTriangle` from lucide-react) + title "Unmapped Materials"
|
|
- Subtitle: "The following materials have no alias to a library material. Map them before rendering."
|
|
- For each unmapped material: row with raw name on left, `<select>` dropdown on right
|
|
- Dropdown pre-populated with `suggestions` from API, plus option to browse all library materials
|
|
3. **State**: `mappings: Record<string, string>` — raw_name → selected material_id
|
|
4. **"Map All & Proceed" button**: disabled until all unmapped materials have a selection; on click calls `batchCreateAliases()`, then `onResolved()` (triggers dispatch)
|
|
5. **Cancel button**: calls `onCancel()`, does not dispatch
|
|
- **Styling**: Follow existing modal patterns, Tailwind classes, lucide-react icons
|
|
- **Acceptance gate**: Dialog renders, dropdowns work, batch alias creation succeeds, dialog closes and render dispatches
|
|
- **Dependencies**: Task 4
|
|
- **Risk**: Low — need to match existing modal patterns
|
|
|
|
### [x] Task 6: Frontend — Intercept "Dispatch Renders" in `OrderDetail.tsx`
|
|
|
|
- **File**: `frontend/src/pages/OrderDetail.tsx`
|
|
- **What**:
|
|
1. Add state for material check data and dialog visibility
|
|
2. Replace direct `dispatchMut.mutate()` on "Dispatch Renders" button with `handleDispatch()`:
|
|
- Calls `checkOrderMaterials(id)`
|
|
- If `unmapped.length > 0`: show `UnmappedMaterialsDialog`
|
|
- If `unmapped.length === 0`: proceed with `dispatchMut.mutate()`
|
|
3. `onResolved` callback: close dialog, call `dispatchMut.mutate()`
|
|
4. Import and render `<UnmappedMaterialsDialog>` conditionally
|
|
- **Acceptance gate**: Dispatch with unmapped materials shows dialog; mapping all and clicking proceed dispatches; dispatch with all mapped skips dialog
|
|
- **Dependencies**: Task 5
|
|
- **Risk**: Low
|
|
|
|
### [x] Task 7: Frontend — Warning badges on Materials page
|
|
|
|
- **File**: `frontend/src/pages/Materials.tsx`
|
|
- **What**:
|
|
1. In the "Custom" materials section, show a warning badge (`AlertTriangle` icon + "No alias") for materials that have no `schaeffler_code` AND no aliases
|
|
2. Add a "Map to Library" action button that opens a dropdown to quickly assign a SCHAEFFLER material as alias
|
|
3. Optional: "Show unmapped only" filter toggle
|
|
- **Acceptance gate**: Custom materials without aliases show a warning; mapping removes it
|
|
- **Dependencies**: None (independent)
|
|
- **Risk**: Low
|
|
|
|
## Migration Check
|
|
|
|
**No** — no new database columns needed. `MaterialAlias` model already exists. All changes use existing tables.
|
|
|
|
## Order Recommendation
|
|
|
|
1. Backend service function (Task 1)
|
|
2. Backend endpoints (Tasks 2-3, parallel)
|
|
3. Frontend API types (Task 4)
|
|
4. Frontend dialog + integration (Tasks 5-6)
|
|
5. Materials page badges (Task 7, independent)
|
|
|
|
## Risks / Open Questions
|
|
|
|
1. **Performance**: `find_unmapped_materials()` loads all aliases and library materials per check. For the current scale (~50 materials, ~100 aliases) this is fine. If scale grows, add caching.
|
|
|
|
2. **Material name normalization**: Should the check be case-insensitive? Yes — alias lookup is already case-insensitive, so the check should match.
|
|
|
|
3. **Products without cad_part_materials**: Some products may not have materials assigned yet (no STEP file processed). These are skipped — the check only validates materials that exist.
|