Files
HartOMat/plan.md
T
Hartmut b583b0d7a2 feat: per-position camera settings, material alias dialog, product delete, media browser links
- 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>
2026-03-14 12:16:37 +01:00

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.