chore(git): resolve merge conflicts
Some checks failed
Staging Build / build (push) Has been cancelled

This commit is contained in:
DanielS
2026-08-20 17:39:37 +02:00
8 changed files with 1013 additions and 795 deletions

View File

@@ -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">

View File

@@ -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) {