Compare commits
3 Commits
9593cbdb79
...
2f32d80e9e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f32d80e9e | ||
|
|
4598233cf4 | ||
|
|
edb5873e33 |
@@ -241,10 +241,10 @@ export function OrderWizard({
|
||||
}, [categories, selectedBillingInterval])
|
||||
|
||||
useEffect(() => {
|
||||
if (visibleCategories.length > 0 && !visibleCategories.some(c => c.id === activeCategoryId)) {
|
||||
if (step === 3 && visibleCategories.length > 0) {
|
||||
setActiveCategoryId(visibleCategories[0].id)
|
||||
}
|
||||
}, [visibleCategories, activeCategoryId])
|
||||
}, [step, visibleCategories])
|
||||
|
||||
// Endkunden Filterung
|
||||
const filteredEndCustomers = useMemo(() => {
|
||||
@@ -659,6 +659,24 @@ export function OrderWizard({
|
||||
setSelections(resetSels)
|
||||
}
|
||||
|
||||
const resetFormForNewDevice = () => {
|
||||
setEditingIdx(null)
|
||||
setDeviceName('')
|
||||
setLicenseNumber('')
|
||||
setModuleQuantities({})
|
||||
const resetSels: Record<string, CategorySelection> = {}
|
||||
categories.forEach(cat => {
|
||||
const isVisible = selectedBillingInterval === 'one_time' ? cat.show_in_kauf !== false : cat.show_in_abo !== false
|
||||
if (!isVisible || cat.allow_multiselect || cat.preselect === false) {
|
||||
resetSels[cat.id] = { productId: null, productIds: [], moduleIds: [] }
|
||||
} else {
|
||||
const first = products.find(p => p.category_id === cat.id && (selectedBillingInterval === 'one_time' ? p.show_in_kauf !== false : p.show_in_abo !== false))
|
||||
resetSels[cat.id] = { productId: first?.id ?? null, productIds: first ? [first.id] : [], moduleIds: [] }
|
||||
}
|
||||
})
|
||||
setSelections(resetSels)
|
||||
}
|
||||
|
||||
const editBasketItem = (idx: number) => {
|
||||
const item = basketItems[idx]
|
||||
if (!item) return
|
||||
@@ -671,6 +689,18 @@ export function OrderWizard({
|
||||
setSelectedBillingInterval(item.billingInterval)
|
||||
}
|
||||
|
||||
const handleEditFromSummary = (idx: number) => {
|
||||
editBasketItem(idx)
|
||||
setDirection(-1)
|
||||
setStep(3)
|
||||
}
|
||||
|
||||
const handleAddAnotherFromSummary = () => {
|
||||
resetFormForNewDevice()
|
||||
setDirection(-1)
|
||||
setStep(3)
|
||||
}
|
||||
|
||||
const deleteBasketItem = (idx: number) => {
|
||||
if (editingIdx === idx) {
|
||||
setEditingIdx(null)
|
||||
@@ -1097,6 +1127,8 @@ export function OrderWizard({
|
||||
linkedFeeProducts={linkedFeeProducts}
|
||||
orderNotes={orderNotes}
|
||||
setOrderNotes={setOrderNotes}
|
||||
onEditBasketItem={handleEditFromSummary}
|
||||
onAddNewBasketItem={handleAddAnotherFromSummary}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -32,6 +32,8 @@ interface StepSummaryProps {
|
||||
linkedFeeProducts?: Product[]
|
||||
orderNotes: string
|
||||
setOrderNotes: (notes: string) => void
|
||||
onEditBasketItem?: (idx: number) => void
|
||||
onAddNewBasketItem?: () => void
|
||||
}
|
||||
|
||||
function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) {
|
||||
@@ -63,6 +65,8 @@ export function StepSummary({
|
||||
linkedFeeProducts = [],
|
||||
orderNotes,
|
||||
setOrderNotes,
|
||||
onEditBasketItem,
|
||||
onAddNewBasketItem,
|
||||
}: StepSummaryProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 h-full items-stretch text-left">
|
||||
@@ -152,11 +156,24 @@ export function StepSummary({
|
||||
<div className="lg:col-span-7 flex flex-col h-full overflow-hidden pl-4">
|
||||
<Card className="glass-dark border-white/10 shadow-2xl overflow-hidden rounded-2xl flex flex-col h-full">
|
||||
<CardHeader className="shrink-0 border-b border-white/5 pb-4">
|
||||
<CardTitle className="text-lg text-white flex items-center justify-between font-bold">
|
||||
<span>Zusammenfassung der Positionen</span>
|
||||
<Badge className="bg-primary/20 text-primary border border-primary/30 text-xs rounded-full">
|
||||
{finalItemsToShow.length} {finalItemsToShow.length === 1 ? 'Kasse' : 'Kassen'}
|
||||
</Badge>
|
||||
<CardTitle className="text-lg text-white flex items-center justify-between font-bold flex-wrap gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Zusammenfassung der Positionen</span>
|
||||
<Badge className="bg-primary/20 text-primary border border-primary/30 text-xs rounded-full">
|
||||
{finalItemsToShow.length} {finalItemsToShow.length === 1 ? 'Kasse' : 'Kassen'}
|
||||
</Badge>
|
||||
</div>
|
||||
{onAddNewBasketItem && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onAddNewBasketItem}
|
||||
className="border-dashed border-primary/50 text-primary hover:bg-primary/10 text-xs font-bold gap-1.5 h-8 rounded-lg"
|
||||
>
|
||||
<Icons.UserPlus className="w-3.5 h-3.5" /> + Weitere Kasse anlegen
|
||||
</Button>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
@@ -165,11 +182,31 @@ export function StepSummary({
|
||||
<div className="space-y-6">
|
||||
{finalItemsToShow.map((item, itemIdx) => (
|
||||
<div key={itemIdx} className="space-y-3 border-b border-white/5 pb-5 last:border-0 last:pb-0">
|
||||
<div className="flex justify-between items-center bg-white/5 p-3 rounded-xl border border-white/10">
|
||||
<span className="text-white font-bold text-sm">Kasse: {item.deviceName}</span>
|
||||
<span className={`text-[10px] px-2 py-0.5 rounded border ${item.billingInterval === '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.billingInterval === 'one_time' ? 'Kauf' : 'Abo'}
|
||||
</span>
|
||||
<div
|
||||
onClick={() => onEditBasketItem && onEditBasketItem(itemIdx)}
|
||||
className={`flex justify-between items-center bg-white/5 p-3 rounded-xl border border-white/10 ${onEditBasketItem ? 'cursor-pointer hover:border-primary/50 hover:bg-white/10 transition-all group' : ''}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-white font-bold text-sm">Kasse: {item.deviceName}</span>
|
||||
{item.licenseNumber && (
|
||||
<span className="text-[10px] text-slate-400">({item.licenseNumber})</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-[10px] px-2 py-0.5 rounded border ${item.billingInterval === '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.billingInterval === 'one_time' ? 'Kauf' : 'Abo'}
|
||||
</span>
|
||||
{onEditBasketItem && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs text-slate-300 group-hover:text-primary group-hover:bg-primary/20 gap-1 rounded-lg"
|
||||
>
|
||||
<Icons.Pencil className="w-3 h-3" /> Bearbeiten
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{visibleCategories.map(cat => {
|
||||
|
||||
@@ -80,15 +80,6 @@ export function SummarySidebar({
|
||||
nextStep,
|
||||
prevStep,
|
||||
}: SummarySidebarProps) {
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
const [selectedBasketIdx, setSelectedBasketIdx] = React.useState<number | null>(null)
|
||||
|
||||
// When editing a basket item via pencil, open the modal for that item
|
||||
const handleEditBasketItem = (idx: number) => {
|
||||
editBasketItem(idx)
|
||||
setIsOpen(true)
|
||||
}
|
||||
|
||||
const fmt = (val: number) => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val)
|
||||
|
||||
return (
|
||||
@@ -256,92 +247,43 @@ export function SummarySidebar({
|
||||
{err}
|
||||
</p>
|
||||
))}
|
||||
</div> {/* Warenkorb List (Scrollable) */}
|
||||
{basketItems.length > 0 && (
|
||||
<div className="shrink-0 pt-2 pb-0 border-t border-white/10 space-y-1.5">
|
||||
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||
<span className="font-semibold uppercase tracking-wider">Kassen ({basketItems.length}):</span>
|
||||
<span className="text-[10px] text-slate-500">Zum Bearbeiten anklicken</span>
|
||||
</div> {/* Kassenname & Lizenznummer Direkt-Eingabe */}
|
||||
<div className="pt-3 border-t border-white/10 space-y-2.5">
|
||||
<p className="text-xs font-bold text-white uppercase tracking-wider">
|
||||
{editingIdx !== null ? `Kasse ${editingIdx + 1} bearbeiten` : `Kassenbezeichnung`}
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="device-name-sidebar" className="text-[11px] text-slate-400">Kassenname (optional)</Label>
|
||||
<Input
|
||||
id="device-name-sidebar"
|
||||
placeholder={`z.B. Kasse ${basketItems.length + 1}, Theke`}
|
||||
value={deviceName}
|
||||
onChange={e => setDeviceName(e.target.value)}
|
||||
className="bg-white/5 border-white/10 text-white text-xs h-8 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 max-h-36 overflow-y-auto pr-1 scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
|
||||
{basketItems.map((item, idx) => {
|
||||
const isEditing = idx === editingIdx
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
onClick={() => editBasketItem(idx)}
|
||||
className={`flex items-center justify-between p-2.5 rounded-xl border transition-all duration-200 text-left cursor-pointer group ${
|
||||
isEditing
|
||||
? 'border-primary bg-primary/15 text-white shadow-[0_0_12px_rgba(59,130,246,0.25)]'
|
||||
: 'border-white/10 bg-slate-900/60 text-slate-300 hover:border-white/20 hover:bg-slate-900'
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0 pr-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-bold text-xs text-white truncate">{item.deviceName}</span>
|
||||
<span className="text-[9px] px-1.5 py-0.2 rounded border border-white/10 text-slate-400 bg-white/5 uppercase">
|
||||
{item.billingInterval === 'one_time' ? 'Kauf' : 'Abo'}
|
||||
</span>
|
||||
{isEditing && (
|
||||
<span className="text-[9px] px-1.5 py-0.2 rounded bg-primary text-slate-950 font-extrabold uppercase">
|
||||
Aktiv
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.licenseNumber && (
|
||||
<p className="text-[10px] text-slate-400 truncate mt-0.5">Lizenz: {item.licenseNumber}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-6 h-6 hover:bg-primary/20 text-slate-400 hover:text-primary rounded-lg transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
editBasketItem(idx)
|
||||
setIsOpen(true)
|
||||
}}
|
||||
title="Name / Lizenz bearbeiten"
|
||||
>
|
||||
<Pencil className="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-6 h-6 hover:bg-red-500/20 text-slate-400 hover:text-red-400 rounded-lg transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
deleteBasketItem(idx)
|
||||
}}
|
||||
title="Löschen"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="license-number-sidebar" className="text-[11px] text-slate-400">Lizenznummer (optional)</Label>
|
||||
<Input
|
||||
id="license-number-sidebar"
|
||||
placeholder="z.B. LIZ-12345 (eindeutig)"
|
||||
value={licenseNumber}
|
||||
onChange={e => setLicenseNumber(e.target.value)}
|
||||
className="bg-white/5 border-white/10 text-white text-xs h-8 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-2 shrink-0 p-4 pt-1.5 border-t border-white/10 bg-slate-950/80">
|
||||
<CardFooter className="flex flex-col gap-2 shrink-0 p-4 pt-2 border-t border-white/10 bg-slate-950/80">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full border-dashed border-primary/50 text-primary hover:bg-primary/10 transition-all rounded-xl h-9 text-xs font-bold gap-1.5 shadow-sm"
|
||||
onClick={() => setIsOpen(true)}
|
||||
disabled={!hasActiveSelection && isNextStepDisabled}
|
||||
>
|
||||
<UserPlus className="w-3.5 h-3.5" /> {editingIdx !== null ? '💾 Kasse speichern' : '+ Weitere Kasse hinzufügen'}
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full h-10 text-sm font-bold bg-gradient-to-r from-blue-600 via-sky-500 to-cyan-400 hover:opacity-90 shadow-[0_0_20px_rgba(56,189,248,0.3)] transition-all"
|
||||
onClick={nextStep}
|
||||
disabled={basketItems.length === 0 && isNextStepDisabled}
|
||||
className="w-full h-11 text-sm font-bold bg-gradient-to-r from-blue-600 via-sky-500 to-cyan-400 hover:opacity-90 shadow-[0_0_20px_rgba(56,189,248,0.3)] transition-all"
|
||||
onClick={async () => {
|
||||
if (hasActiveSelection || editingIdx !== null) {
|
||||
await addToBasket()
|
||||
}
|
||||
nextStep()
|
||||
}}
|
||||
disabled={isNextStepDisabled && basketItems.length === 0}
|
||||
>
|
||||
Weiter zu Schritt 4 <ChevronRight className="ml-1 w-4 h-4" />
|
||||
</Button>
|
||||
@@ -350,61 +292,6 @@ export function SummarySidebar({
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent className="bg-slate-900 border border-slate-800 text-white rounded-2xl max-w-md p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-bold text-white">
|
||||
{editingIdx !== null ? 'Kasse bearbeiten' : 'Kasse anlegen'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-400 text-xs mt-1">
|
||||
Geben Sie den Namen und die Lizenznummer für diese Kasse ein.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="device-name-modal" className="text-xs text-slate-400">Kassenname (optional)</Label>
|
||||
<Input
|
||||
id="device-name-modal"
|
||||
placeholder={`z.B. Kasse ${basketItems.length + 1}, Theke`}
|
||||
value={deviceName}
|
||||
onChange={e => setDeviceName(e.target.value)}
|
||||
className="bg-white/5 border-white/10 text-white text-xs h-9 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="license-number-modal" className="text-xs text-slate-400">Lizenznummer (optional)</Label>
|
||||
<Input
|
||||
id="license-number-modal"
|
||||
placeholder="z.B. LIZ-12345 (eindeutig)"
|
||||
value={licenseNumber}
|
||||
onChange={e => setLicenseNumber(e.target.value)}
|
||||
className="bg-white/5 border-white/10 text-white text-xs h-9 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="flex flex-row justify-end gap-2 mt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="text-slate-400 hover:text-white"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="h-10"
|
||||
onClick={() => {
|
||||
addToBasket()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
Speichern
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user