chore(git): resolve merge conflicts
Some checks failed
Staging Build / build (push) Has been cancelled
Some checks failed
Staging Build / build (push) Has been cancelled
This commit is contained in:
@@ -545,128 +545,150 @@ export function OrdersTable({ initialOrders }: OrdersTableProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Geräte & Produktauswahl (Snapshot Aufteilung) */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-bold text-slate-400 uppercase tracking-wider flex items-center gap-1.5">
|
||||
<Package className="w-4 h-4 text-primary" />
|
||||
Konfigurierte Kassen & Module ({displayItems.length})
|
||||
</div>
|
||||
{selectedOrder.status === 'in_review' && !isEditingSnapshot && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setIsEditingSnapshot(true)}
|
||||
className="h-7 border-primary/30 text-primary hover:bg-primary/10 text-xs font-bold gap-1 rounded-md"
|
||||
>
|
||||
<Edit3 className="w-3 h-3" /> Preise anpassen
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{/* Geräte & Produktauswahl (Nach Kassen / Boxen gruppiert) */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-bold text-slate-400 uppercase tracking-wider flex items-center gap-1.5">
|
||||
<Package className="w-4 h-4 text-primary" />
|
||||
Konfigurierte Kassen ({displayItems.length})
|
||||
</div>
|
||||
{selectedOrder.status === 'in_review' && !isEditingSnapshot && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setIsEditingSnapshot(true)}
|
||||
className="h-7 border-primary/30 text-primary hover:bg-primary/10 text-xs font-bold gap-1 rounded-md"
|
||||
>
|
||||
<Edit3 className="w-3 h-3" /> Preise anpassen
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditingSnapshot && (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between bg-amber-500/10 border border-amber-500/20 p-3 rounded-xl gap-4 animate-pulse">
|
||||
<span className="text-xs text-amber-400 font-semibold">Sie befinden sich im Preiskorrektur-Modus.</span>
|
||||
{isEditingSnapshot && (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between bg-amber-500/10 border border-amber-500/20 p-3 rounded-xl gap-4 animate-pulse">
|
||||
<span className="text-xs text-amber-400 font-semibold">Sie befinden sich im Preiskorrektur-Modus.</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setEditedSnapshot(JSON.parse(JSON.stringify(selectedOrder.order_data || {})));
|
||||
setIsEditingSnapshot(false);
|
||||
}}
|
||||
className="h-7 text-xs text-slate-300 hover:text-white"
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSaveSnapshot}
|
||||
disabled={isSavingSnapshot}
|
||||
className="h-7 text-xs bg-amber-600 hover:bg-amber-500 text-white font-bold"
|
||||
>
|
||||
{isSavingSnapshot ? <Loader2 className="w-3 h-3 animate-spin mr-1" /> : null}
|
||||
Änderungen speichern
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{displayItems.map((item: any, idx: number) => {
|
||||
const devName = item.device_name || `Kasse #${idx + 1}`
|
||||
const licNum = item.license_number
|
||||
const modules = item.selected_modules || []
|
||||
|
||||
return (
|
||||
<div key={idx} className="p-4 rounded-xl bg-slate-900/90 border border-white/10 space-y-3 relative overflow-hidden">
|
||||
{/* Box Header für die Kasse */}
|
||||
<div className="flex items-center justify-between border-b border-white/5 pb-2.5 flex-wrap gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setEditedSnapshot(JSON.parse(JSON.stringify(selectedOrder.order_data || {})));
|
||||
setIsEditingSnapshot(false);
|
||||
}}
|
||||
className="h-7 text-xs text-slate-300 hover:text-white"
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSaveSnapshot}
|
||||
disabled={isSavingSnapshot}
|
||||
className="h-7 text-xs bg-amber-600 hover:bg-amber-500 text-white font-bold"
|
||||
>
|
||||
{isSavingSnapshot ? <Loader2 className="w-3 h-3 animate-spin mr-1" /> : null}
|
||||
Änderungen speichern
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{displayItems.map((item: any, idx: number) => (
|
||||
<div key={idx} className="p-4 rounded-xl bg-slate-900/90 border border-white/10 space-y-3">
|
||||
<div className="flex items-center justify-between border-b border-white/5 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-primary/20 text-primary border border-primary/30">
|
||||
{item.device_name || `Kasse #${idx + 1}`}
|
||||
</span>
|
||||
<span className="font-bold text-sm text-white">{item.product_name}</span>
|
||||
</div>
|
||||
|
||||
{isEditingSnapshot ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor={`price-${idx}`} className="text-[10px] text-slate-400">Preis (€):</Label>
|
||||
<Input
|
||||
id={`price-${idx}`}
|
||||
type="number"
|
||||
value={item.base_price || 0}
|
||||
onChange={(e) => updateItemBasePrice(idx, parseFloat(e.target.value) || 0)}
|
||||
className="w-20 h-7 bg-slate-950 border-white/10 text-white text-xs px-2"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs font-bold text-slate-200">
|
||||
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(item.base_price || 0)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Ausgewählte Module & Lizenzen */}
|
||||
{item.selected_modules?.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-[11px] font-semibold text-slate-400 flex items-center gap-1">
|
||||
<Layers className="w-3 h-3 text-slate-500" />
|
||||
Zusatzmodule ({item.selected_modules.length}):
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{item.selected_modules.map((mod: any, mIdx: number) => (
|
||||
<div key={mIdx} className="p-2 rounded-lg bg-slate-950/60 border border-white/5 text-xs flex items-center justify-between gap-3">
|
||||
<span className="text-slate-300 font-medium truncate">
|
||||
{mod.module_name || mod.name}
|
||||
</span>
|
||||
|
||||
{isEditingSnapshot ? (
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<Input
|
||||
type="number"
|
||||
title="Einzelpreis"
|
||||
value={mod.price || 0}
|
||||
onChange={(e) => updateModulePriceOrQty(idx, mIdx, 'price', parseFloat(e.target.value) || 0)}
|
||||
className="w-14 h-6 bg-slate-900 border-white/10 text-white text-[10px] px-1"
|
||||
/>
|
||||
<span className="text-[10px] text-slate-500">x</span>
|
||||
<Input
|
||||
type="number"
|
||||
title="Menge"
|
||||
value={mod.quantity || 1}
|
||||
onChange={(e) => updateModulePriceOrQty(idx, mIdx, 'quantity', parseInt(e.target.value) || 1)}
|
||||
className="w-10 h-6 bg-slate-900 border-white/10 text-white text-[10px] px-1"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-400 font-mono text-[11px] shrink-0">
|
||||
+{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price * (mod.quantity || 1))}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<span className="px-2.5 py-1 rounded text-xs font-extrabold bg-blue-500/20 text-blue-400 border border-blue-500/30 font-mono">
|
||||
{devName}
|
||||
</span>
|
||||
{licNum && (
|
||||
<span className="text-xs font-mono text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20 flex items-center gap-1">
|
||||
<span className="text-[10px] text-slate-400 uppercase font-sans">Lizenz:</span>
|
||||
{licNum}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<span className={`text-[10px] px-2 py-0.5 rounded border ${item.billing_interval === 'one_time' ? 'border-amber-500/30 text-amber-400 bg-amber-500/10' : 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'} font-semibold uppercase`}>
|
||||
{item.billing_interval === 'one_time' ? 'Kauf' : 'Abo'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Inhalt der Kasse: Hauptprodukt + Preis */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold text-sm text-white">{item.product_name}</span>
|
||||
{isEditingSnapshot ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor={`price-${idx}`} className="text-[10px] text-slate-400 font-sans">Preis (€):</Label>
|
||||
<Input
|
||||
id={`price-${idx}`}
|
||||
type="number"
|
||||
value={item.base_price || 0}
|
||||
onChange={(e) => updateItemBasePrice(idx, parseFloat(e.target.value) || 0)}
|
||||
className="w-20 h-7 bg-slate-950 border-white/10 text-white text-xs px-2 font-mono"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
(item.price || item.base_price) && (
|
||||
<span className="text-xs font-bold text-slate-200 font-mono">
|
||||
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(item.price || item.base_price)}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Module dieser Kasse */}
|
||||
{modules.length > 0 && (
|
||||
<div className="space-y-1.5 pt-2 border-t border-white/5">
|
||||
<div className="text-[11px] font-semibold text-slate-400 flex items-center gap-1">
|
||||
<Layers className="w-3 h-3 text-slate-500" />
|
||||
Enthaltene Module ({modules.length}):
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{modules.map((mod: any, mIdx: number) => (
|
||||
<div key={mIdx} className="p-2 rounded-lg bg-slate-950/60 border border-white/5 text-xs flex items-center justify-between gap-3">
|
||||
<span className="text-slate-300 font-medium truncate">
|
||||
+ {mod.module_name || mod.name} {!isEditingSnapshot && mod.quantity > 1 ? `(${mod.quantity}x)` : ''}
|
||||
</span>
|
||||
{isEditingSnapshot ? (
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<Input
|
||||
type="number"
|
||||
title="Einzelpreis"
|
||||
value={mod.price || 0}
|
||||
onChange={(e) => updateModulePriceOrQty(idx, mIdx, 'price', parseFloat(e.target.value) || 0)}
|
||||
className="w-14 h-6 bg-slate-900 border-white/10 text-white text-[10px] px-1 font-mono"
|
||||
/>
|
||||
<span className="text-[10px] text-slate-500 font-sans">x</span>
|
||||
<Input
|
||||
type="number"
|
||||
title="Menge"
|
||||
value={mod.quantity || 1}
|
||||
onChange={(e) => updateModulePriceOrQty(idx, mIdx, 'quantity', parseInt(e.target.value) || 1)}
|
||||
className="w-10 h-6 bg-slate-900 border-white/10 text-white text-[10px] px-1 font-mono"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
mod.price && (
|
||||
<span className="text-slate-400 font-mono text-[11px] shrink-0">
|
||||
+{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price * (mod.quantity || 1))}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflow Freigabe & Aktionen */}
|
||||
<div className="p-4 rounded-xl bg-slate-900/90 border border-white/10 space-y-3">
|
||||
|
||||
@@ -429,11 +429,15 @@ export function OrderWizard({
|
||||
})
|
||||
}, [visibleCategories, selections, products, selectedBillingInterval])
|
||||
|
||||
const finalItemsToShow = basketItems.length > 0
|
||||
? basketItems
|
||||
: (allCategoriesFilled && productValidationErrors.length === 0
|
||||
? [{ deviceName: deviceName || 'Kasse 1', selections, moduleQuantities, billingInterval: selectedBillingInterval }]
|
||||
: [])
|
||||
const finalItemsToShow = useMemo(() => {
|
||||
if (basketItems.length > 0) {
|
||||
return basketItems
|
||||
}
|
||||
if (allCategoriesFilled && productValidationErrors.length === 0 && hasActiveSelection) {
|
||||
return [{ deviceName: deviceName || 'Kasse 1', licenseNumber, selections, moduleQuantities, billingInterval: selectedBillingInterval }]
|
||||
}
|
||||
return []
|
||||
}, [basketItems, allCategoriesFilled, productValidationErrors, hasActiveSelection, deviceName, licenseNumber, selections, moduleQuantities, selectedBillingInterval])
|
||||
|
||||
// Gesamtsummen für alle Items in der Bestellung (finalItemsToShow)
|
||||
const { overallMonthlyTotal, overallOneTimeTotal, linkedFeeProducts } = useMemo(() => {
|
||||
@@ -542,7 +546,7 @@ export function OrderWizard({
|
||||
|
||||
const nextStep = () => {
|
||||
if (step === 3 && allCategoriesFilled && productValidationErrors.length === 0) {
|
||||
const isDirtyNewDevice = deviceName.trim() !== '' || licenseNumber.trim() !== '' || Object.values(moduleQuantities).some(q => q > 0)
|
||||
const isDirtyNewDevice = (deviceName.trim() !== '' || licenseNumber.trim() !== '' || Object.values(moduleQuantities).some(q => q > 0)) && editingIdx === null
|
||||
if (editingIdx !== null) {
|
||||
addToBasket()
|
||||
} else if (basketItems.length === 0 && hasActiveSelection) {
|
||||
@@ -743,35 +747,35 @@ export function OrderWizard({
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
let finalItems = [...basketItems]
|
||||
const isDirtyNewDevice = deviceName.trim() !== '' || licenseNumber.trim() !== '' || Object.values(moduleQuantities).some(q => q > 0)
|
||||
if ((editingIdx !== null || basketItems.length === 0 || isDirtyNewDevice) && hasActiveSelection && allCategoriesFilled && productValidationErrors.length === 0) {
|
||||
const isDirtyNewDevice = (deviceName.trim() !== '' || licenseNumber.trim() !== '' || Object.values(moduleQuantities).some(q => q > 0)) && editingIdx === null
|
||||
if (editingIdx !== null) {
|
||||
const normLicense = licenseNumber.trim().toUpperCase()
|
||||
if (normLicense) {
|
||||
const isDuplicateInBasket = basketItems.some(
|
||||
(item, idx) => idx !== editingIdx && item.licenseNumber?.toUpperCase().trim() === normLicense
|
||||
)
|
||||
if (isDuplicateInBasket) {
|
||||
throw new Error(`Lizenznummer "${licenseNumber}" ist bereits im aktuellen Warenkorb vorhanden.`)
|
||||
}
|
||||
|
||||
const isTaken = await isLicenseNumberTaken(normLicense, initialOrder?.id)
|
||||
if (isTaken) {
|
||||
throw new Error(`Lizenznummer "${licenseNumber}" wird bereits von einer anderen Bestellung verwendet.`)
|
||||
}
|
||||
}
|
||||
|
||||
const currentItem = {
|
||||
deviceName: deviceName || (editingIdx !== null ? basketItems[editingIdx]?.deviceName : `Kasse ${basketItems.length + 1}`),
|
||||
deviceName: deviceName || basketItems[editingIdx]?.deviceName || `Kasse ${editingIdx + 1}`,
|
||||
licenseNumber: normLicense,
|
||||
selections: JSON.parse(JSON.stringify(selections)),
|
||||
moduleQuantities: { ...moduleQuantities },
|
||||
billingInterval: selectedBillingInterval,
|
||||
}
|
||||
if (editingIdx !== null) {
|
||||
finalItems[editingIdx] = currentItem
|
||||
} else {
|
||||
finalItems.push(currentItem)
|
||||
}
|
||||
finalItems[editingIdx] = currentItem
|
||||
} else if (basketItems.length === 0 && hasActiveSelection && allCategoriesFilled && productValidationErrors.length === 0) {
|
||||
const normLicense = licenseNumber.trim().toUpperCase()
|
||||
finalItems.push({
|
||||
deviceName: deviceName || 'Kasse 1',
|
||||
licenseNumber: normLicense,
|
||||
selections: JSON.parse(JSON.stringify(selections)),
|
||||
moduleQuantities: { ...moduleQuantities },
|
||||
billingInterval: selectedBillingInterval,
|
||||
})
|
||||
} else if (isDirtyNewDevice && hasActiveSelection && allCategoriesFilled && productValidationErrors.length === 0) {
|
||||
const normLicense = licenseNumber.trim().toUpperCase()
|
||||
finalItems.push({
|
||||
deviceName: deviceName || `Kasse ${basketItems.length + 1}`,
|
||||
licenseNumber: normLicense,
|
||||
selections: JSON.parse(JSON.stringify(selections)),
|
||||
moduleQuantities: { ...moduleQuantities },
|
||||
billingInterval: selectedBillingInterval,
|
||||
})
|
||||
}
|
||||
|
||||
if (finalItems.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user