diff --git a/frontend/src/components/admin/GlobalRenderPositionsPanel.tsx b/frontend/src/components/admin/GlobalRenderPositionsPanel.tsx index 1b12030..afae697 100644 --- a/frontend/src/components/admin/GlobalRenderPositionsPanel.tsx +++ b/frontend/src/components/admin/GlobalRenderPositionsPanel.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import React, { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Plus, Pencil, Trash2, Check, X, Copy } from 'lucide-react' import { @@ -19,6 +19,7 @@ interface EditState { is_default: boolean sort_order: number focal_length_mm: number | null + sensor_width_mm: number | null } const EMPTY_EDIT: EditState = { @@ -30,6 +31,7 @@ const EMPTY_EDIT: EditState = { is_default: false, sort_order: 0, focal_length_mm: null, + sensor_width_mm: null, } export default function GlobalRenderPositionsPanel() { @@ -44,7 +46,7 @@ export default function GlobalRenderPositionsPanel() { const createMut = useMutation({ mutationFn: (body: GlobalRenderPositionCreate) => createGlobalRenderPosition(body), - onSuccess: () => { qc.invalidateQueries({ queryKey: ['global-render-positions'] }); setAdding(false) }, + onSuccess: () => { qc.invalidateQueries({ queryKey: ['global-render-positions'] }); setAdding(false); setEditing(null) }, }) const updateMut = useMutation({ @@ -69,6 +71,7 @@ export default function GlobalRenderPositionsPanel() { is_default: pos.is_default, sort_order: pos.sort_order, focal_length_mm: pos.focal_length_mm, + sensor_width_mm: pos.sensor_width_mm, }) } @@ -96,23 +99,101 @@ export default function GlobalRenderPositionsPanel() { setAdding(false) } - function rotField(label: string, field: keyof Pick) { + function renderEditFormGrid() { if (!editing) return null return ( -
- - setEditing({ ...editing, [field]: parseFloat(e.target.value) || 0 })} - /> +
+ {/* Row 1: Name + Is Default */} +
+ + setEditing({ ...editing, name: e.target.value })} + /> +
+
+ + +
+ + {/* Row 2: Rotation X, Y, Z */} +
+ + setEditing({ ...editing, rotation_x: parseFloat(e.target.value) || 0 })} + /> +
+
+ + setEditing({ ...editing, rotation_y: parseFloat(e.target.value) || 0 })} + /> +
+
+ + setEditing({ ...editing, rotation_z: parseFloat(e.target.value) || 0 })} + /> +
+ + {/* Row 3: Focal Length, Sensor Width, Sort Order */} +
+ + setEditing({ ...editing, focal_length_mm: e.target.value ? parseFloat(e.target.value) : null })} + /> +
+
+ + setEditing({ ...editing, sensor_width_mm: e.target.value ? parseFloat(e.target.value) : null })} + /> +
+
+ + setEditing({ ...editing, sort_order: parseInt(e.target.value) || 0 })} + /> +
) } - if (isLoading) return

Loading…

+ if (isLoading) return

Loading...

return (
@@ -139,147 +220,99 @@ export default function GlobalRenderPositionsPanel() { - {positions.map((pos) => { - const isEditingThis = editing && editing.id === pos.id - return ( - - {isEditingThis ? ( - <> - - setEditing({ ...editing!, name: e.target.value })} - /> - - {rotField('', 'rotation_x')} - {rotField('', 'rotation_y')} - {rotField('', 'rotation_z')} - - setEditing({ ...editing!, focal_length_mm: e.target.value ? parseFloat(e.target.value) : null })} - /> - - - setEditing({ ...editing!, is_default: e.target.checked })} - /> - - - setEditing({ ...editing!, sort_order: parseInt(e.target.value) || 0 })} - /> - - - - - - - ) : ( - <> - {pos.name} - {pos.rotation_x} - {pos.rotation_y} - {pos.rotation_z} - - {pos.focal_length_mm != null ? pos.focal_length_mm : 50} - - - {pos.is_default && } - - {pos.sort_order} - - - - - - - )} + {/* Add new — expandable form at top */} + {adding && editing && ( + <> + + +
New Position
+ {renderEditFormGrid()} +
+ + +
+ + + )} + + {positions.map((pos) => { + const isEditingThis = !adding && editing !== null && editing.id === pos.id + return ( + + {/* Display row — always visible */} + + {pos.name} + {pos.rotation_x} + {pos.rotation_y} + {pos.rotation_z} + + {pos.focal_length_mm != null ? pos.focal_length_mm : 50} + + + {pos.is_default && } + + {pos.sort_order} + + + + + + + + {/* Expandable edit form row */} + {isEditingThis && ( + + + {renderEditFormGrid()} +
+ + +
+ + + )} +
) })} - - {/* New row */} - {adding && editing && ( - - - setEditing({ ...editing, name: e.target.value })} - /> - - {rotField('', 'rotation_x')} - {rotField('', 'rotation_y')} - {rotField('', 'rotation_z')} - - setEditing({ ...editing, focal_length_mm: e.target.value ? parseFloat(e.target.value) : null })} - /> - - - setEditing({ ...editing, is_default: e.target.checked })} - /> - - - setEditing({ ...editing, sort_order: parseInt(e.target.value) || 0 })} - /> - - - - - - - )}
diff --git a/frontend/src/components/admin/PricingTierTable.tsx b/frontend/src/components/admin/PricingTierTable.tsx index 5e70ffa..cc1dcfa 100644 --- a/frontend/src/components/admin/PricingTierTable.tsx +++ b/frontend/src/components/admin/PricingTierTable.tsx @@ -1,18 +1,29 @@ -import { useState } from 'react' +import React, { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { Pencil, Trash2, Plus, Check, X } from 'lucide-react' +import { Pencil, Trash2, Plus } from 'lucide-react' import { toast } from 'sonner' import { listPricingTiers, createPricingTier, updatePricingTier, deletePricingTier } from '../../api/pricing' import type { PricingTier } from '../../api/pricing' -const EMPTY_FORM = { category_key: '', quality_level: 'Normal', price_per_item: '', description: '' } +const ALL_CATEGORIES = [ + { key: 'default', label: 'default (Global Fallback)' }, + { key: 'TRB', label: 'TRB' }, + { key: 'Kugellager', label: 'Kugellager' }, + { key: 'CRB', label: 'CRB' }, + { key: 'Gleitlager', label: 'Gleitlager' }, + { key: 'SRB_TORB', label: 'SRB/TORB' }, + { key: 'Linear_schiene', label: 'Linear' }, + { key: 'Anschlagplatten', label: 'Anschlag' }, +] + +const EMPTY_FORM = { category_key: '', quality_level: 'Normal', price_per_item: '', description: '', is_active: true } export default function PricingTierTable() { const qc = useQueryClient() const [showAdd, setShowAdd] = useState(false) const [form, setForm] = useState(EMPTY_FORM) const [editingId, setEditingId] = useState(null) - const [editDraft, setEditDraft] = useState>({}) + const [editDraft, setEditDraft] = useState & { _price_str?: string }>({}) const { data: tiers, isLoading } = useQuery({ queryKey: ['pricing-tiers'], @@ -26,6 +37,7 @@ export default function PricingTierTable() { quality_level: form.quality_level.trim() || 'Normal', price_per_item: parseFloat(form.price_per_item), description: form.description.trim() || undefined, + is_active: form.is_active, }), onSuccess: () => { toast.success('Pricing tier created') @@ -37,14 +49,16 @@ export default function PricingTierTable() { }) const updateMut = useMutation({ - mutationFn: ({ id, data }: { id: number; data: Partial }) => - updatePricingTier(id, { - category_key: data.category_key, - quality_level: data.quality_level, - price_per_item: data.price_per_item != null ? Number(data.price_per_item) : undefined, - description: data.description !== undefined ? data.description ?? undefined : undefined, - is_active: data.is_active, - }), + mutationFn: ({ id, data }: { id: number; data: Partial & { _price_str?: string } }) => { + const { _price_str, ...rest } = data + return updatePricingTier(id, { + category_key: rest.category_key, + quality_level: rest.quality_level, + price_per_item: rest.price_per_item != null ? Number(rest.price_per_item) : undefined, + description: rest.description !== undefined ? rest.description ?? undefined : undefined, + is_active: rest.is_active, + }) + }, onSuccess: () => { toast.success('Tier updated') qc.invalidateQueries({ queryKey: ['pricing-tiers'] }) @@ -71,6 +85,7 @@ export default function PricingTierTable() { price_per_item: tier.price_per_item, description: tier.description ?? '', is_active: tier.is_active, + _price_str: String(tier.price_per_item), }) } @@ -81,70 +96,175 @@ export default function PricingTierTable() { const canCreate = form.category_key.trim() !== '' && form.price_per_item !== '' && !isNaN(parseFloat(form.price_per_item)) - return ( -
- {/* Add form toggle */} -
- {showAdd ? ( -
-
+ /* Shared edit form grid — used by both edit-row and add-row */ + function renderEditFormGrid(mode: 'add' | 'edit', tier: PricingTier | null) { + const catVal = mode === 'edit' ? (editDraft.category_key ?? tier?.category_key ?? '') : form.category_key + const qualVal = mode === 'edit' ? (editDraft.quality_level ?? tier?.quality_level ?? '') : form.quality_level + const priceVal = mode === 'edit' ? (editDraft._price_str ?? String(tier?.price_per_item ?? '')) : form.price_per_item + const descVal = mode === 'edit' ? (editDraft.description ?? tier?.description ?? '') : form.description + const activeVal = mode === 'edit' ? (editDraft.is_active ?? tier?.is_active ?? true) : form.is_active + + const setCat = (v: string) => mode === 'edit' ? setEditDraft((d) => ({ ...d, category_key: v })) : setForm((f) => ({ ...f, category_key: v })) + const setQual = (v: string) => mode === 'edit' ? setEditDraft((d) => ({ ...d, quality_level: v })) : setForm((f) => ({ ...f, quality_level: v })) + const setPrice = (v: string) => { + if (mode === 'edit') { + setEditDraft((d) => ({ ...d, _price_str: v, price_per_item: v ? parseFloat(v) : undefined })) + } else { + setForm((f) => ({ ...f, price_per_item: v })) + } + } + const setDesc = (v: string) => mode === 'edit' ? setEditDraft((d) => ({ ...d, description: v })) : setForm((f) => ({ ...f, description: v })) + const setActive = (v: boolean) => mode === 'edit' ? setEditDraft((d) => ({ ...d, is_active: v })) : setForm((f) => ({ ...f, is_active: v })) + + return ( +
+ {/* Row 1: Category Key, Quality Level, Price per Item */} +
+
+ + +
+
+ + setQual(e.target.value)} + placeholder="e.g. Normal, Premium" + className="input-base w-full" + /> +
+
+ +
+ setForm({ ...form, category_key: e.target.value })} - className="input-base" - title="Product category key this tier applies to (e.g. TRB, Kugellager). Leave empty for the global fallback tier." - /> - setForm({ ...form, quality_level: e.target.value })} - className="input-base" - title="Quality level label for this tier (e.g. Normal, Premium). Used for display purposes." - /> - setForm({ ...form, price_per_item: e.target.value })} - className="input-base" + value={priceVal} + onChange={(e) => setPrice(e.target.value)} + placeholder="0.00" + className="input-base w-full pl-7" /> - setForm({ ...form, description: e.target.value })} - className="input-base" - /> -
-
- -
- ) : ( - - )} +
+ + {/* Row 2: Description (full width) */} +
+ +