feat: per-line material override in product order wizard Step 3

- Added "Mat Override" column to the review table
- Each line has its own dropdown (per-line takes priority over global)
- Default shows global override if set, otherwise "No override"
- "Clear" option to explicitly remove override on a line when global is set
- Amber background when override is active

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 14:52:21 +01:00
parent 24833ce52e
commit dbadfdf489
+27 -7
View File
@@ -50,6 +50,7 @@ export default function NewProductOrderPage() {
const [globalPositionSelections, setGlobalPositionSelections] = useState<GlobalPositionSelections>({})
const [notes, setNotes] = useState('')
const [materialOverride, setMaterialOverride] = useState<string>('')
const [lineOverrides, setLineOverrides] = useState<Record<string, string>>({})
const [submitting, setSubmitting] = useState(false)
// ---- Step 1: load products with STEP files ----
@@ -383,13 +384,17 @@ export default function NewProductOrderPage() {
try {
const result = await createOrder({
notes: notes || undefined,
lines: orderLines.map((l) => ({
product_id: l.product.id,
output_type_id: l.outputType.id,
render_position_id: l.position?.id ?? null,
global_render_position_id: l.globalPosition?.id ?? null,
material_override: materialOverride || null,
})),
lines: orderLines.map((l) => {
const lineOv = lineOverrides[l.key]
const override = lineOv === '__none__' ? null : (lineOv || materialOverride || null)
return {
product_id: l.product.id,
output_type_id: l.outputType.id,
render_position_id: l.position?.id ?? null,
global_render_position_id: l.globalPosition?.id ?? null,
material_override: override,
}
}),
})
toast.success(`Draft order ${result.order_number} created — review and submit`)
navigate(`/orders/${result.id}`)
@@ -773,6 +778,7 @@ export default function NewProductOrderPage() {
<th className="px-4 py-3 font-medium text-content-secondary">Position</th>
<th className="px-4 py-3 font-medium text-content-secondary">Renderer</th>
<th className="px-4 py-3 font-medium text-content-secondary">Format</th>
<th className="px-4 py-3 font-medium text-content-secondary">Mat Override</th>
<th className="px-4 py-3 font-medium text-content-secondary text-right">Price</th>
<th className="px-4 py-3 font-medium text-content-secondary w-12"></th>
</tr>
@@ -809,6 +815,20 @@ export default function NewProductOrderPage() {
</td>
<td className="px-4 py-3 text-content-muted">{line.outputType.renderer}</td>
<td className="px-4 py-3 text-content-muted uppercase">{line.outputType.output_format}</td>
<td className="px-4 py-3">
<select
className="text-xs border border-border-default rounded px-1.5 py-1 w-full"
style={{ backgroundColor: (lineOverrides[line.key] || materialOverride) ? 'rgba(245, 158, 11, 0.1)' : 'var(--color-bg-surface)' }}
value={lineOverrides[line.key] ?? ''}
onChange={(e) => setLineOverrides((prev) => ({ ...prev, [line.key]: e.target.value }))}
>
<option value="">{materialOverride ? `Global: ${materialOverride.replace('SCHAEFFLER_', '').replace(/_/g, ' ')}` : 'No override'}</option>
{materialOverride && <option value="__none__"> No override (clear) </option>}
{libMaterials.map((m: Material) => (
<option key={m.id} value={m.name}>{m.name.replace('SCHAEFFLER_', '').replace(/_/g, ' ')}</option>
))}
</select>
</td>
<td className="px-4 py-3 text-right">
{(() => {
const price = getLinePrice(line.product.id, line.outputType.id)