refactor(wizard): optimize ui ux with no-page-scroll and hub flow
All checks were successful
Staging Build / build (push) Successful in 3m16s

This commit is contained in:
DanielS
2026-09-07 22:26:13 +02:00
parent 2a1897141e
commit 694e8dfbc1
6 changed files with 1523 additions and 1022 deletions

View File

@@ -202,7 +202,7 @@ export function OrderWizard({
// Formular-State für neuen Endkunden // Formular-State für neuen Endkunden
const [newCustomerForm, setNewCustomerForm] = useState({ const [newCustomerForm, setNewCustomerForm] = useState({
company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '', company_name: '', vat_id: '', email: '', first_name: '', last_name: '', street: '', zip: '', city: '',
bank_iban: '', bank_bic: '', bank_name: '', bank_owner: '', bank_iban: '', bank_bic: '', bank_name: '', bank_owner: '',
}) })
const [isCreatingCustomer, setIsCreatingCustomer] = useState(false) const [isCreatingCustomer, setIsCreatingCustomer] = useState(false)
@@ -614,7 +614,7 @@ export function OrderWizard({
setSelectedEndCustomerId(created.id) setSelectedEndCustomerId(created.id)
setCustomerMode('select') setCustomerMode('select')
setNewCustomerForm({ setNewCustomerForm({
company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '', company_name: '', vat_id: '', email: '', first_name: '', last_name: '', street: '', zip: '', city: '',
bank_iban: '', bank_bic: '', bank_name: '', bank_owner: '', bank_iban: '', bank_bic: '', bank_name: '', bank_owner: '',
}) })
} catch (e: any) { } catch (e: any) {
@@ -624,7 +624,7 @@ export function OrderWizard({
} }
} }
const addToBasket = async () => { const addToBasket = async (proceedToSummary: boolean = false) => {
const normLicense = licenseNumber.trim().toUpperCase() const normLicense = licenseNumber.trim().toUpperCase()
if (normLicense) { if (normLicense) {
const isDuplicateInBasket = basketItems.some( const isDuplicateInBasket = basketItems.some(
@@ -676,6 +676,11 @@ export function OrderWizard({
} }
}) })
setSelections(resetSels) setSelections(resetSels)
if (proceedToSummary) {
setDirection(1)
setStep(4)
}
} }
const resetFormForNewDevice = () => { const resetFormForNewDevice = () => {
@@ -1098,6 +1103,8 @@ export function OrderWizard({
existingModuleIds={existingModuleIds} existingModuleIds={existingModuleIds}
activeCategoryId={activeCategoryId} activeCategoryId={activeCategoryId}
editingDeviceName={editingIdx !== null ? (basketItems[editingIdx]?.deviceName || `Kasse ${editingIdx + 1}`) : null} editingDeviceName={editingIdx !== null ? (basketItems[editingIdx]?.deviceName || `Kasse ${editingIdx + 1}`) : null}
onCancelEdit={resetFormForNewDevice}
productValidationErrors={productValidationErrors}
/> />
</div> </div>
</div> </div>
@@ -1122,19 +1129,15 @@ export function OrderWizard({
updatePriceModifier={updatePriceModifier} updatePriceModifier={updatePriceModifier}
allCategoriesFilled={allCategoriesFilled} allCategoriesFilled={allCategoriesFilled}
productValidationErrors={productValidationErrors} productValidationErrors={productValidationErrors}
basketItems={basketItems}
editingIdx={editingIdx} editingIdx={editingIdx}
editBasketItem={editBasketItem}
deleteBasketItem={deleteBasketItem}
deviceName={deviceName} deviceName={deviceName}
setDeviceName={setDeviceName} setDeviceName={setDeviceName}
licenseNumber={licenseNumber} licenseNumber={licenseNumber}
setLicenseNumber={setLicenseNumber} setLicenseNumber={setLicenseNumber}
addToBasket={addToBasket} onSaveAndProceed={() => addToBasket(true)}
isNextStepDisabled={isNextStepDisabled} isNextStepDisabled={isNextStepDisabled}
hasActiveSelection={hasActiveSelection} hasActiveSelection={hasActiveSelection}
showValidationWarning={showValidationWarning} showValidationWarning={showValidationWarning}
nextStep={nextStep}
prevStep={prevStep} prevStep={prevStep}
/> />
</div> </div>

View File

@@ -1,11 +1,24 @@
'use client' 'use client'
import React from 'react' import React from 'react'
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card' import { Card, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { CreditCard, Calendar, Check, ChevronLeft, ChevronRight } from 'lucide-react' import { Badge } from '@/components/ui/badge'
import {
CreditCard,
Calendar,
Check,
ChevronLeft,
ChevronRight,
RefreshCw,
ShoppingBag,
ShieldCheck,
CheckCircle2,
Info,
} from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
interface StepBillingProps { interface StepBillingProps {
selectedBillingInterval: 'one_time' | 'monthly' selectedBillingInterval: 'one_time' | 'monthly'
@@ -28,105 +41,207 @@ export function StepBilling({
prevStep, prevStep,
nextStep, nextStep,
}: StepBillingProps) { }: StepBillingProps) {
// Optionale Validierung falls Datum bei Kauf Pflicht wäre, oder prüfen ob Modell gewählt ist
const isOneTime = selectedBillingInterval === 'one_time'
const isMonthly = selectedBillingInterval === 'monthly'
return ( return (
<Card className="glass-dark border-white/10"> <Card className="glass-dark border-white/10 h-[calc(100vh-8.5rem)] flex flex-col justify-between overflow-hidden relative">
<CardHeader> {/* ─── 1. FIXED HEADER (shrink-0) ─── */}
<CardTitle className="text-2xl flex items-center gap-2 text-white"> <CardHeader className="pb-3 pt-4 px-6 shrink-0 border-b border-white/10 bg-slate-950/40 space-y-0">
<CreditCard className="w-6 h-6 text-primary" /> <div className="flex items-center justify-between gap-3">
<div>
<CardTitle className="text-xl font-bold flex items-center gap-2 text-white">
<CreditCard className="w-5 h-5 text-primary" />
Abrechnungsmodell wählen Abrechnungsmodell wählen
</CardTitle> </CardTitle>
<CardDescription className="text-slate-300"> <CardDescription className="text-xs text-slate-300 mt-0.5">
Möchten Sie Lizenzen dauerhaft kaufen oder monatlich mieten? Wählen Sie zwischen monatlicher Miete (Abonnement) oder einmaligem Softwarekauf.
</CardDescription> </CardDescription>
</div>
<Badge variant="outline" className="border-primary/30 text-primary bg-primary/10 text-xs px-3 py-1">
Schritt 2 von 4
</Badge>
</div>
</CardHeader> </CardHeader>
<CardContent>
<div className="grid md:grid-cols-2 gap-6 py-4"> {/* ─── 2. SCROLLABLE MIDDLE CONTENT (flex-1 min-h-0 overflow-y-auto) ─── */}
{/* Option 1: Kauf */} <div className="flex-1 min-h-0 overflow-y-auto p-6 flex flex-col justify-center max-w-4xl mx-auto w-full scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
<div className="space-y-6 my-auto">
{/* Abrechnungsmodell Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{/* Option 1: Miete / Abo */}
<div <div
onClick={() => handleBillingIntervalChange('one_time')} onClick={() => handleBillingIntervalChange('monthly')}
className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between min-h-[12rem] h-auto hover:shadow-[0_0_20px_rgba(255,255,255,0.05)] ${selectedBillingInterval === 'one_time' className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between min-h-[14rem] select-none ${
? 'border-primary bg-primary/10 shadow-[0_0_25px_rgba(59,130,246,0.2)]' isMonthly
: 'border-white/5 bg-white/5 hover:border-white/20' ? 'border-primary bg-primary/10 shadow-[0_0_20px_rgba(59,130,246,0.2)] ring-1 ring-primary/50'
: 'border-white/5 bg-white/5 hover:border-white/20 hover:bg-white/[0.07]'
}`} }`}
> >
<div className="flex flex-col justify-between h-full">
<div> <div>
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center justify-between gap-3 mb-4">
<div className={`p-3 rounded-xl ${selectedBillingInterval === 'one_time' ? 'bg-primary/20 text-primary' : 'bg-white/5 text-slate-400'}`}> <div className={`p-3.5 rounded-xl ${isMonthly ? 'bg-primary/20 text-primary border border-primary/30' : 'bg-white/5 text-slate-400 border border-white/10'}`}>
<CreditCard className="w-6 h-6" /> <RefreshCw className={`w-6 h-6 ${isMonthly ? 'animate-spin-slow' : ''}`} />
</div> </div>
<h3 className="text-lg font-bold text-white">Einmaliger Kauf</h3> {isMonthly && (
<div className="w-6 h-6 rounded-full bg-primary flex items-center justify-center text-white shadow-md shadow-primary/30">
<Check className="w-3.5 h-3.5" />
</div> </div>
<p className="text-slate-400 text-sm"> )}
Einmalige Anschaffungskosten für die Softwarelizenz. Keine monatlichen Mietgebühren. </div>
<div className="space-y-1">
<div className="flex items-center gap-2">
<h3 className="text-lg font-bold text-white">Miete / Abo</h3>
<Badge variant="outline" className="text-[10px] bg-blue-500/10 border-blue-500/30 text-blue-400">
Empfohlen
</Badge>
</div>
<p className="text-xs font-medium text-slate-300">
Fortlaufende monatliche Abrechnung
</p> </p>
</div> </div>
{selectedBillingInterval === 'one_time' && ( <p className="text-slate-400 text-xs mt-3 leading-relaxed">
<div className="absolute top-4 right-4 w-6 h-6 rounded-full bg-primary flex items-center justify-center text-white"> Maximale Flexibilität: Inklusive sämtlicher Software-Updates, Cloud-Dienste und technischem Support. Jederzeit kündbar oder anpassbar.
<Check className="w-4 h-4" /> </p>
</div>
<div className="mt-4 pt-3 border-t border-white/10 flex items-center justify-between text-xs">
<span className="text-slate-400">Zahlungsintervall:</span>
<span className="text-primary font-semibold">monatlich</span>
</div>
</div>
{/* Option 2: Kauf / Einmalkauf */}
<div
onClick={() => handleBillingIntervalChange('one_time')}
className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between min-h-[14rem] select-none ${
isOneTime
? 'border-primary bg-primary/10 shadow-[0_0_20px_rgba(59,130,246,0.2)] ring-1 ring-primary/50'
: 'border-white/5 bg-white/5 hover:border-white/20 hover:bg-white/[0.07]'
}`}
>
<div>
<div className="flex items-center justify-between gap-3 mb-4">
<div className={`p-3.5 rounded-xl ${isOneTime ? 'bg-primary/20 text-primary border border-primary/30' : 'bg-white/5 text-slate-400 border border-white/10'}`}>
<ShoppingBag className="w-6 h-6" />
</div>
{isOneTime && (
<div className="w-6 h-6 rounded-full bg-primary flex items-center justify-center text-white shadow-md shadow-primary/30">
<Check className="w-3.5 h-3.5" />
</div> </div>
)} )}
</div>
{selectedBillingInterval === 'one_time' && customerMode === 'select' && selectedEndCustomerId && ( <div className="space-y-1">
<div <div className="flex items-center gap-2">
onClick={(e) => e.stopPropagation()} <h3 className="text-lg font-bold text-white">Kauf / Einmalkauf</h3>
className="mt-4 pt-4 border-t border-white/5 space-y-2 cursor-default" <Badge variant="outline" className="text-[10px] bg-slate-500/10 border-slate-500/30 text-slate-300">
Klassisch
</Badge>
</div>
<p className="text-xs font-medium text-slate-300">
Einmalige Lizenzgebühr
</p>
</div>
<p className="text-slate-400 text-xs mt-3 leading-relaxed">
Dauerhafte Lizenzrechte für die gekaufte Version inklusive 12 Monate kostenfreier Updates. Keine laufenden monatlichen Mietgebühren.
</p>
</div>
<div className="mt-4 pt-3 border-t border-white/10 flex items-center justify-between text-xs">
<span className="text-slate-400">Zahlungsintervall:</span>
<span className="text-slate-200 font-semibold">einmalig</span>
</div>
</div>
</div>
{/* Dynamische Stichtagsabfrage bei Kauf */}
<AnimatePresence>
{isOneTime && (
<motion.div
initial={{ opacity: 0, y: 10, height: 0 }}
animate={{ opacity: 1, y: 0, height: 'auto' }}
exit={{ opacity: 0, y: -10, height: 0 }}
transition={{ duration: 0.25 }}
className="overflow-hidden"
> >
<Label htmlFor="last-license-date" className="text-white text-xs font-medium flex items-center gap-2"> <div className="p-5 rounded-2xl bg-gradient-to-r from-slate-900/90 to-slate-900/60 border border-white/10 space-y-3 shadow-lg">
<Calendar className="w-3.5 h-3.5 text-primary" /> <div className="flex items-start gap-3">
Datum der letzten CASPOS-Lizenz <div className="p-2.5 rounded-xl bg-primary/20 text-primary border border-primary/30 shrink-0 mt-0.5">
<Calendar className="w-4 h-4" />
</div>
<div className="space-y-1 flex-1">
<Label htmlFor="last-license-date" className="text-white text-xs font-bold flex items-center gap-2">
Datum des letzten CASPOS-Kaufs (Stichtag)
</Label> </Label>
<p className="text-[11px] text-slate-400 leading-normal">
Datum des letzten Kaufs zur automatischen Berechnung der Update-Staffel (z. B. 15% innerhalb 1 Jahr, 30% bis 2 Jahre, 50% bis 3 Jahre).
</p>
</div>
</div>
<div className="pt-2 flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
<div className="relative flex-1 max-w-sm">
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input <Input
id="last-license-date" id="last-license-date"
type="date" type="date"
value={lastLicenseDate} value={lastLicenseDate}
onChange={e => setLastLicenseDate(e.target.value)} onChange={(e) => setLastLicenseDate(e.target.value)}
className="bg-[#0b1329] border-white/10 text-white text-xs h-8 focus:border-primary" className="pl-9 bg-slate-950/80 border-white/10 text-white text-xs h-9 focus:border-primary"
/> />
<p className="text-[10px] text-slate-500">
Ermöglicht automatische Ermittlung von Update-Gebühren.
</p>
</div> </div>
{lastLicenseDate && (
<Badge variant="outline" className="bg-emerald-500/10 border-emerald-500/30 text-emerald-400 text-xs px-2.5 py-1 flex items-center gap-1.5 self-start sm:self-auto">
<CheckCircle2 className="w-3.5 h-3.5" /> Stichtag gesetzt
</Badge>
)} )}
</div> </div>
</div> </div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
{/* Option 2: Abo */} {/* ─── 3. FIXED BOTTOM ACTION BAR (shrink-0) ─── */}
<div <div className="shrink-0 border-t border-white/10 bg-slate-950/90 backdrop-blur-md px-6 py-3.5 flex flex-col sm:flex-row items-center justify-between gap-3 z-10">
onClick={() => handleBillingIntervalChange('monthly')} {/* Back Button */}
className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between min-h-[12rem] h-auto hover:shadow-[0_0_20px_rgba(255,255,255,0.05)] ${selectedBillingInterval === 'monthly' <Button
? 'border-primary bg-primary/10 shadow-[0_0_25px_rgba(59,130,246,0.2)]' variant="outline"
: 'border-white/5 bg-white/5 hover:border-white/20' onClick={prevStep}
className="w-full sm:w-auto border-white/10 text-white hover:bg-white/10 h-9 text-xs gap-1.5"
>
<ChevronLeft className="w-4 h-4" /> Zurück zu Schritt 1
</Button>
{/* Selected Model Status Chip */}
<div className="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-white/5 border border-white/10 text-white text-xs">
<span className="text-slate-400">Gewähltes Modell:</span>
<Badge
variant="outline"
className={`text-xs px-2 py-0.5 ${
isMonthly
? 'border-blue-500/30 text-blue-400 bg-blue-500/10'
: 'border-slate-500/30 text-slate-300 bg-slate-500/10'
}`} }`}
> >
<div> {isMonthly ? 'Monatliches Abo' : 'Einmaliger Kauf'}
<div className="flex items-center gap-3 mb-3"> </Badge>
<div className={`p-3 rounded-xl ${selectedBillingInterval === 'monthly' ? 'bg-primary/20 text-primary' : 'bg-white/5 text-slate-400'}`}>
<Calendar className="w-6 h-6" />
</div> </div>
<h3 className="text-lg font-bold text-white">Monatliches Abo (Miete)</h3>
</div> {/* Next Button */}
<p className="text-slate-400 text-sm"> <Button
Laufende monatliche Gebühren. Inklusive aller Updates und flexibler Laufzeit. onClick={nextStep}
</p> className="w-full sm:w-auto bg-primary hover:bg-primary/90 text-white font-semibold px-6 shadow-lg shadow-primary/20 h-9 text-xs gap-1.5"
</div> >
{selectedBillingInterval === 'monthly' && ( Weiter zu Schritt 3 <ChevronRight className="w-4 h-4" />
<div className="absolute top-4 right-4 w-6 h-6 rounded-full bg-primary flex items-center justify-center text-white">
<Check className="w-4 h-4" />
</div>
)}
</div>
</div>
</CardContent>
<CardFooter className="flex justify-between border-t border-white/10 pt-6">
<Button variant="ghost" className="text-white" onClick={prevStep}>
<ChevronLeft className="mr-2 w-4 h-4" /> Zurück
</Button> </Button>
<Button onClick={nextStep}> </div>
Weiter zur Software <ChevronRight className="ml-2 w-4 h-4" />
</Button>
</CardFooter>
</Card> </Card>
) )
} }

View File

@@ -2,7 +2,7 @@
import React, { useState, useMemo, useEffect } from 'react' import React, { useState, useMemo, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion' import { motion, AnimatePresence } from 'framer-motion'
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card' import { Card, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -19,6 +19,14 @@ import {
ChevronsLeft, ChevronsLeft,
ChevronsRight, ChevronsRight,
Users, Users,
ChevronDown,
ChevronUp,
X,
CreditCard,
Mail,
MapPin,
CheckCircle2,
Building,
} from 'lucide-react' } from 'lucide-react'
import { EndCustomer } from '@/lib/types' import { EndCustomer } from '@/lib/types'
@@ -65,6 +73,18 @@ export function StepCustomer({
const [partnerPageSize, setPartnerPageSize] = useState<number>(10) const [partnerPageSize, setPartnerPageSize] = useState<number>(10)
const [partnerCurrentPage, setPartnerCurrentPage] = useState<number>(1) const [partnerCurrentPage, setPartnerCurrentPage] = useState<number>(1)
// Drawer / Sheet state for customer creation
const [showBankDetails, setShowBankDetails] = useState(false)
// Drawer visibility is mapped to customerMode === 'create'
const isDrawerOpen = customerMode === 'create'
const openDrawer = () => setCustomerMode('create')
const closeDrawer = () => {
setCustomerMode('select')
setShowBankDetails(false)
}
useEffect(() => { useEffect(() => {
setPartnerCurrentPage(1) setPartnerCurrentPage(1)
}, [partnerSearchTerm, partnerPageSize]) }, [partnerSearchTerm, partnerPageSize])
@@ -123,7 +143,8 @@ export function StepCustomer({
c.first_name?.toLowerCase().includes(term) || c.first_name?.toLowerCase().includes(term) ||
c.last_name?.toLowerCase().includes(term) || c.last_name?.toLowerCase().includes(term) ||
c.city?.toLowerCase().includes(term) || c.city?.toLowerCase().includes(term) ||
c.zip?.toLowerCase().includes(term) c.zip?.toLowerCase().includes(term) ||
c.email?.toLowerCase().includes(term)
) )
}, [customersByCompany, searchTerm]) }, [customersByCompany, searchTerm])
@@ -135,6 +156,11 @@ export function StepCustomer({
return filteredCustomers.slice(from, from + pageSize) return filteredCustomers.slice(from, from + pageSize)
}, [filteredCustomers, currentPage, pageSize]) }, [filteredCustomers, currentPage, pageSize])
// Selected customer object for summary chip / info
const activeCustomer = useMemo(() => {
return endCustomers.find((c) => c.id === selectedEndCustomerId) || null
}, [endCustomers, selectedEndCustomerId])
// Click handler for customer selection // Click handler for customer selection
const handleSelectCustomer = (customer: EndCustomer) => { const handleSelectCustomer = (customer: EndCustomer) => {
setSelectedEndCustomerId(customer.id) setSelectedEndCustomerId(customer.id)
@@ -143,119 +169,112 @@ export function StepCustomer({
} }
} }
// Handle form submission with enter or button
const onSubmitCustomerForm = async (e: React.FormEvent) => {
e.preventDefault()
if (!newCustomerForm.company_name?.trim()) return
await handleCreateCustomer()
}
return ( return (
<Card className="glass-dark border-white/10"> <>
<CardHeader className="pb-3"> <Card className="glass-dark border-white/10 h-[calc(100vh-8.5rem)] flex flex-col overflow-hidden relative">
<CardTitle className="text-2xl flex items-center gap-2 text-white"> {/* ─── 1. FIXED HEADER (shrink-0) ─── */}
<User className="w-6 h-6 text-primary" /> <CardHeader className="pb-3 pt-4 px-6 shrink-0 border-b border-white/10 bg-slate-950/40 space-y-0">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div>
<CardTitle className="text-xl font-bold flex items-center gap-2 text-white">
<User className="w-5 h-5 text-primary" />
Für wen bestellen Sie? Für wen bestellen Sie?
</CardTitle> </CardTitle>
<CardDescription className="text-slate-300"> <CardDescription className="text-xs text-slate-300 mt-0.5">
Wählen Sie den Partner (für Admins) und den Endkunden aus oder legen Sie einen neuen an. Wählen Sie den Endkunden für die Lizenzierung aus oder legen Sie schnell einen neuen Datensatz an.
</CardDescription> </CardDescription>
</CardHeader> </div>
<CardContent className="space-y-6">
{/* Modus-Toggle: Bestandskunde vs. Neuer Kunde */}
<div className="flex gap-2 pb-2">
<Button <Button
variant={customerMode === 'select' ? 'default' : 'ghost'} onClick={openDrawer}
size="sm" size="sm"
onClick={() => setCustomerMode('select')} className="gap-2 shrink-0 bg-primary hover:bg-primary/90 text-white font-medium shadow-md shadow-primary/20 h-8 text-xs"
className={`gap-2 ${customerMode === 'select' ? '' : 'text-white'}`}
> >
<Building2 className="w-4 h-4" /> Bestandskunde wählen <UserPlus className="w-4 h-4" /> Kunde anlegen
</Button>
<Button
variant={customerMode === 'create' ? 'default' : 'ghost'}
size="sm"
onClick={() => setCustomerMode('create')}
className={`gap-2 ${customerMode === 'create' ? '' : 'text-white'}`}
>
<UserPlus className="w-4 h-4" /> Neuen Kunden anlegen
</Button> </Button>
</div> </div>
</CardHeader>
<div className={isAdmin ? 'grid grid-cols-1 lg:grid-cols-2 gap-8 items-stretch' : 'space-y-6'}> {/* ─── 2. SCROLLABLE MIDDLE CONTENT (flex-1 min-h-0 overflow-hidden) ─── */}
{/* ─── 1. ADMIN PARTNER SELECTION ─── */} <div className="flex-1 min-h-0 overflow-hidden p-6">
<div className={`h-full ${isAdmin ? 'grid grid-cols-1 lg:grid-cols-2 gap-6 items-stretch' : 'flex flex-col'}`}>
{/* ─── 2A. ADMIN PARTNER SELECTION ─── */}
{isAdmin && ( {isAdmin && (
<div className="p-5 rounded-2xl bg-white/5 border border-white/10 flex flex-col h-[34rem] overflow-hidden justify-between space-y-4"> <div className="p-4 rounded-2xl bg-white/5 border border-white/10 flex flex-col h-full overflow-hidden justify-between space-y-3">
<div className="flex items-center justify-between gap-4 flex-wrap"> <div className="flex items-center justify-between gap-4 flex-wrap shrink-0">
<div> <div>
<Label className="text-white font-bold text-base flex items-center gap-2"> <Label className="text-white font-bold text-sm flex items-center gap-2">
<Building2 className="w-5 h-5 text-primary" /> <Building2 className="w-4 h-4 text-primary" />
Partner-Auswahl (Admin-Funktion) Partner-Auswahl (Admin)
</Label> </Label>
<p className="text-xs text-slate-400 mt-0.5"> <p className="text-[11px] text-slate-400 mt-0.5">
Wählen Sie einen Partner aus, um dessen Kunden anzuzeigen, oder &quot;Alle Partner&quot;. Filtert die Kundenliste nach Partner
</p> </p>
</div> </div>
<Badge variant="outline" className="border-primary/30 text-primary bg-primary/10"> <Badge variant="outline" className="border-primary/30 text-primary bg-primary/10 text-[10px]">
Admin Modus Admin Modus
</Badge> </Badge>
</div> </div>
{/* Partner Header Controls: Search & Page Size */} {/* Partner Search & Page Size */}
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center justify-between"> <div className="flex gap-2 items-center shrink-0">
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-slate-400" />
<Input <Input
placeholder="Partner suchen..." placeholder="Partner suchen..."
value={partnerSearchTerm} value={partnerSearchTerm}
onChange={(e) => setPartnerSearchTerm(e.target.value)} onChange={(e) => setPartnerSearchTerm(e.target.value)}
className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-500 focus:border-primary text-sm h-9" className="pl-8 bg-white/5 border-white/10 text-white placeholder:text-slate-500 focus:border-primary text-xs h-8"
/> />
</div> </div>
<div className="flex items-center gap-2 self-end sm:self-auto">
<span className="text-xs text-slate-400 whitespace-nowrap">Partner pro Seite:</span>
<select <select
value={partnerPageSize} value={partnerPageSize}
onChange={(e) => setPartnerPageSize(Number(e.target.value))} onChange={(e) => setPartnerPageSize(Number(e.target.value))}
className="h-9 rounded-md border border-white/10 bg-slate-900 px-2 py-1 text-xs text-white shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary" className="h-8 rounded-md border border-white/10 bg-slate-900 px-2 text-[11px] text-white focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary shrink-0"
> >
<option value={10}>10</option> <option value={10}>10</option>
<option value={20}>20</option> <option value={20}>20</option>
<option value={50}>50</option> <option value={50}>50</option>
</select> </select>
</div> </div>
</div>
{/* Partner Card List */} {/* Partner Card List (Scrollable) */}
<div className="space-y-2 flex-1 overflow-y-auto pr-1 min-h-0 py-1"> <div className="space-y-1.5 flex-1 min-h-0 overflow-y-auto pr-1 py-1 scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
{/* Reserved Fixed Card: Alle Partner */} {/* Reserved Fixed Card: Alle Partner */}
<div <div
onClick={() => { onClick={() => {
setSelectedCompanyId('all') setSelectedCompanyId('all')
setSelectedEndCustomerId(null) setSelectedEndCustomerId(null)
}} }}
className={`flex items-center justify-between p-3.5 rounded-xl border-2 cursor-pointer transition-all select-none ${ className={`flex items-center justify-between p-2.5 rounded-xl border cursor-pointer transition-all select-none ${
selectedCompanyId === 'all' || !selectedCompanyId selectedCompanyId === 'all' || !selectedCompanyId
? 'border-primary bg-primary/10 shadow-lg shadow-primary/5' ? 'border-primary bg-primary/10 shadow-sm shadow-primary/10'
: 'border-white/5 bg-white/5 hover:bg-white/10' : 'border-white/5 bg-white/5 hover:bg-white/10'
}`} }`}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-lg bg-primary/20 flex items-center justify-center text-primary font-bold shrink-0"> <div className="w-7 h-7 rounded-lg bg-primary/20 flex items-center justify-center text-primary font-bold shrink-0">
<Users className="w-4 h-4" /> <Users className="w-3.5 h-3.5" />
</div> </div>
<div> <div>
<p className="font-semibold text-white text-sm"> <p className="font-semibold text-white text-xs">
Alle Partner (Firmenübergreifend) Alle Partner (Firmenübergreifend)
</p> </p>
<p className="text-xs text-slate-400"> <p className="text-[11px] text-slate-400">
Zeigt Kunden aller registrierten Partnerunternehmen an {endCustomers.filter((c) => !c.is_anonymized).length} Kunden gesamt
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="border-white/10 text-slate-300 text-xs bg-white/5">
{endCustomers.filter((c) => !c.is_anonymized).length} Kunden gesamt
</Badge>
{(selectedCompanyId === 'all' || !selectedCompanyId) && ( {(selectedCompanyId === 'all' || !selectedCompanyId) && (
<Check className="w-5 h-5 text-primary shrink-0" /> <Check className="w-4 h-4 text-primary shrink-0" />
)} )}
</div> </div>
</div>
{/* Dynamic Partner Cards */} {/* Dynamic Partner Cards */}
{paginatedCompanies.map((c: any) => { {paginatedCompanies.map((c: any) => {
@@ -269,135 +288,141 @@ export function StepCustomer({
setSelectedCompanyId(c.id) setSelectedCompanyId(c.id)
setSelectedEndCustomerId(null) setSelectedEndCustomerId(null)
}} }}
className={`flex items-center justify-between p-3.5 rounded-xl border-2 cursor-pointer transition-all select-none ${ className={`flex items-center justify-between p-2.5 rounded-xl border cursor-pointer transition-all select-none ${
isSelected isSelected
? 'border-primary bg-primary/10 shadow-lg shadow-primary/5' ? 'border-primary bg-primary/10 shadow-sm shadow-primary/10'
: 'border-white/5 bg-white/5 hover:bg-white/10' : 'border-white/5 bg-white/5 hover:bg-white/10'
}`} }`}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-2.5 min-w-0">
<div className="w-8 h-8 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center text-slate-300 font-bold shrink-0"> <div className="w-7 h-7 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center text-slate-300 font-bold shrink-0">
<Building2 className="w-4 h-4 text-slate-400" /> <Building2 className="w-3.5 h-3.5 text-slate-400" />
</div> </div>
<div> <div className="min-w-0">
<p className="font-semibold text-white text-sm">{c.name}</p> <p className="font-semibold text-white text-xs truncate">{c.name}</p>
{(c.street || c.zip || c.city) && ( {(c.street || c.zip || c.city) && (
<p className="text-xs text-slate-400 mt-0.5"> <p className="text-[11px] text-slate-400 truncate">
{[c.street, [c.zip, c.city].filter(Boolean).join(' ')].filter(Boolean).join(', ')} {[c.street, [c.zip, c.city].filter(Boolean).join(' ')].filter(Boolean).join(', ')}
</p> </p>
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-1.5 shrink-0 ml-2">
<Badge <Badge
variant="outline" variant="outline"
className={`text-xs ${ className={`text-[10px] px-1.5 py-0 ${
customerCount > 0 customerCount > 0
? 'border-green-500/30 text-green-400 bg-green-500/10' ? 'border-green-500/30 text-green-400 bg-green-500/10'
: 'border-white/10 text-slate-500 bg-white/5' : 'border-white/10 text-slate-500 bg-white/5'
}`} }`}
> >
{customerCount} {customerCount === 1 ? 'Kunde' : 'Kunden'} {customerCount}
</Badge> </Badge>
{isSelected && <Check className="w-5 h-5 text-primary shrink-0" />} {isSelected && <Check className="w-4 h-4 text-primary shrink-0" />}
</div> </div>
</div> </div>
) )
})} })}
{filteredCompanies.length === 0 && ( {filteredCompanies.length === 0 && (
<div className="p-4 text-center text-slate-400 text-xs bg-white/5 rounded-xl border border-white/5"> <div className="p-3 text-center text-slate-400 text-xs bg-white/5 rounded-xl border border-white/5 my-auto">
Keine Partner mit dem Suchbegriff &quot;{partnerSearchTerm}&quot; gefunden. Keine Partner gefunden.
</div> </div>
)} )}
</div> </div>
{/* Partner Pagination Controls */} {/* Partner Pagination Footer */}
<div className="flex items-center justify-between border-t border-white/10 pt-3 text-xs text-slate-400 flex-wrap gap-2"> <div className="flex items-center justify-between border-t border-white/10 pt-2 text-[11px] text-slate-400 shrink-0">
<div> <div>
Zeige {totalPartnerCount === 0 ? 0 : (partnerCurrentPage - 1) * partnerPageSize + 1} bis{' '} {Math.min(partnerCurrentPage * partnerPageSize, totalPartnerCount)} / {totalPartnerCount} Partner
{Math.min(partnerCurrentPage * partnerPageSize, totalPartnerCount)} von {totalPartnerCount} Partnern
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => setPartnerCurrentPage(1)} onClick={() => setPartnerCurrentPage(1)}
disabled={partnerCurrentPage === 1} disabled={partnerCurrentPage === 1}
className="h-8 px-2 text-slate-400 hover:text-white" className="h-6 px-1.5 text-slate-400 hover:text-white"
title="Erste Seite"
> >
<ChevronsLeft className="w-4 h-4" /> <ChevronsLeft className="w-3.5 h-3.5" />
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setPartnerCurrentPage((p) => Math.max(1, p - 1))} onClick={() => setPartnerCurrentPage((p) => Math.max(1, p - 1))}
disabled={partnerCurrentPage === 1} disabled={partnerCurrentPage === 1}
className="h-8 px-2.5 border-white/10 text-white" className="h-6 px-2 border-white/10 text-white text-[11px]"
> >
<ChevronLeft className="w-3.5 h-3.5 mr-1" /> Zurück <ChevronLeft className="w-3 h-3 mr-0.5" /> Zurück
</Button> </Button>
<span className="px-1.5 text-slate-300">
<span className="px-2 font-medium text-slate-300"> {partnerCurrentPage} / {totalPartnerPages}
Seite {partnerCurrentPage} von {totalPartnerPages}
</span> </span>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setPartnerCurrentPage((p) => Math.min(totalPartnerPages, p + 1))} onClick={() => setPartnerCurrentPage((p) => Math.min(totalPartnerPages, p + 1))}
disabled={partnerCurrentPage === totalPartnerPages} disabled={partnerCurrentPage === totalPartnerPages}
className="h-8 px-2.5 border-white/10 text-white" className="h-6 px-2 border-white/10 text-white text-[11px]"
> >
Vor <ChevronRight className="w-3.5 h-3.5 ml-1" /> Vor <ChevronRight className="w-3 h-3 ml-0.5" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => setPartnerCurrentPage(totalPartnerPages)} onClick={() => setPartnerCurrentPage(totalPartnerPages)}
disabled={partnerCurrentPage === totalPartnerPages} disabled={partnerCurrentPage === totalPartnerPages}
className="h-8 px-2 text-slate-400 hover:text-white" className="h-6 px-1.5 text-slate-400 hover:text-white"
title="Letzte Seite"
> >
<ChevronsRight className="w-4 h-4" /> <ChevronsRight className="w-3.5 h-3.5" />
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
)} )}
<AnimatePresence mode="wait"> {/* ─── 2B. CUSTOMER SELECTION LIST (Scrollable Panel) ─── */}
{/* ─── 2. CUSTOMER SELECTION ─── */} <div className="p-4 rounded-2xl bg-white/5 border border-white/10 flex flex-col h-full overflow-hidden justify-between space-y-3">
{customerMode === 'select' && ( <div className="flex items-center justify-between gap-3 shrink-0">
<motion.div <div className="min-w-0">
key="select" <Label className="text-white font-bold text-sm flex items-center gap-2">
initial={{ opacity: 0, y: 10 }} <User className="w-4 h-4 text-primary" />
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="p-5 rounded-2xl bg-white/5 border border-white/10 flex flex-col h-[34rem] overflow-hidden justify-between space-y-4"
>
<div className="flex items-center justify-between gap-4 flex-wrap">
<div>
<Label className="text-white font-bold text-base flex items-center gap-2">
<User className="w-5 h-5 text-primary" />
Kunden-Auswahl Kunden-Auswahl
</Label> </Label>
<p className="text-xs text-slate-400 mt-0.5"> <p className="text-[11px] text-slate-400 mt-0.5 truncate">
{isAdmin && selectedCompanyId && selectedCompanyId !== 'all' {isAdmin && selectedCompanyId && selectedCompanyId !== 'all'
? `Endkunden für den Partner "${companies.find((c) => c.id === selectedCompanyId)?.name || selectedCompanyId}"` ? `Partner: "${companies.find((c) => c.id === selectedCompanyId)?.name || selectedCompanyId}"`
: 'Alle verfügbaren Endkunden'} : 'Alle Endkunden'}
</p> </p>
</div> </div>
<Button
variant="outline"
size="sm"
onClick={openDrawer}
className="border-primary/40 text-primary hover:bg-primary/10 h-7 text-xs gap-1.5 shrink-0"
>
<UserPlus className="w-3 h-3" /> Neuer Kunde
</Button>
</div> </div>
{/* Customer Search */}
<div className="relative shrink-0">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-slate-400" />
<Input
placeholder="Kunden suchen nach Name, Ort, PLZ, E-Mail..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 bg-white/5 border-white/10 text-white placeholder:text-slate-500 text-xs h-8"
/>
</div>
{/* Customer Card List (Isolated Scrollable Area) */}
<div className="flex-1 min-h-0 overflow-y-auto pr-1 space-y-2 pb-2 scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
{customersByCompany.length === 0 ? ( {customersByCompany.length === 0 ? (
<div className="p-6 rounded-xl bg-white/5 border border-white/10 text-center space-y-3"> <div className="p-6 rounded-xl bg-white/5 border border-white/10 text-center space-y-3 my-auto">
<Building2 className="w-10 h-10 text-slate-600 mx-auto" /> <Building2 className="w-8 h-8 text-slate-600 mx-auto" />
<p className="text-slate-400"> <p className="text-slate-400 text-xs">
{isAdmin && selectedCompanyId && selectedCompanyId !== 'all' {isAdmin && selectedCompanyId && selectedCompanyId !== 'all'
? 'Keine Endkunden für den ausgewählten Partner vorhanden.' ? 'Keine Endkunden für den ausgewählten Partner vorhanden.'
: 'Noch keine Endkunden vorhanden.'} : 'Noch keine Endkunden vorhanden.'}
@@ -405,59 +430,27 @@ export function StepCustomer({
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCustomerMode('create')} onClick={openDrawer}
className="border-white/10 text-white" className="border-white/10 text-white text-xs h-8"
> >
<UserPlus className="w-4 h-4 mr-2" /> Ersten Kunden anlegen <UserPlus className="w-3.5 h-3.5 mr-1.5" /> Ersten Kunden anlegen
</Button> </Button>
</div> </div>
) : ( ) : filteredCustomers.length === 0 ? (
<div className="space-y-4 flex-1 flex flex-col min-h-0 overflow-hidden justify-between"> <div className="p-4 text-center text-slate-400 text-xs bg-white/5 rounded-xl border border-white/5 my-auto">
{/* Customer Header Controls: Search & Page Size */} Keine passenden Kunden für &quot;{searchTerm}&quot; gefunden.
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center justify-between">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
placeholder="Kunden suchen nach Name, Ort, PLZ..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-500 text-sm h-9"
/>
</div>
{/* Page Size Selector */}
<div className="flex items-center gap-2 self-end sm:self-auto">
<span className="text-xs text-slate-400 whitespace-nowrap">Kunden pro Seite:</span>
<select
value={pageSize}
onChange={(e) => setPageSize(Number(e.target.value))}
className="h-9 rounded-md border border-white/10 bg-slate-900 px-2 py-1 text-xs text-white shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary"
>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
</select>
</div>
</div>
{filteredCustomers.length === 0 ? (
<div className="p-4 text-center text-slate-400 text-xs bg-white/5 rounded-xl border border-white/5">
Keine passenden Kunden gefunden.
</div> </div>
) : ( ) : (
<div className="flex-1 flex flex-col min-h-0 justify-between"> paginatedCustomers.map((customer) => {
{/* Customer Card List */}
<div className="space-y-2 flex-1 overflow-y-auto pr-1 min-h-0 py-1">
{paginatedCustomers.map((customer) => {
const isSelected = selectedEndCustomerId === customer.id const isSelected = selectedEndCustomerId === customer.id
return ( return (
<div <div
key={customer.id} key={customer.id}
onClick={() => handleSelectCustomer(customer)} onClick={() => handleSelectCustomer(customer)}
className={`flex items-start p-3.5 rounded-xl border-2 cursor-pointer transition-all select-none ${ className={`flex items-start p-3 rounded-xl border-2 cursor-pointer transition-all select-none ${
isSelected isSelected
? 'border-primary bg-primary/10 shadow-lg shadow-primary/5' ? 'border-primary bg-primary/10 shadow-md shadow-primary/5'
: 'border-white/5 bg-white/5 hover:bg-white/10' : 'border-white/5 bg-white/5 hover:bg-white/10'
}`} }`}
> >
@@ -469,35 +462,52 @@ export function StepCustomer({
onChange={() => handleSelectCustomer(customer)} onChange={() => handleSelectCustomer(customer)}
className="sr-only" className="sr-only"
/> />
<div className="flex-1"> <div className="flex-1 min-w-0 pr-2">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<p className="font-semibold text-white text-sm">{customer.company_name}</p> <p className="font-semibold text-white text-xs truncate">{customer.company_name}</p>
{isAdmin && customer.partner_id && ( {isAdmin && customer.partner_id && (
<Badge <Badge
variant="outline" variant="outline"
className="text-[10px] border-white/10 text-slate-400 bg-white/5" className="text-[9px] px-1.5 py-0 border-white/10 text-slate-400 bg-white/5"
> >
Partner: {companies.find((co) => co.id === customer.partner_id)?.name || customer.partner_id} Partner: {companies.find((co) => co.id === customer.partner_id)?.name || customer.partner_id}
</Badge> </Badge>
)} )}
</div> </div>
<p className="text-xs text-slate-400 mt-0.5"> <p className="text-[11px] text-slate-400 mt-0.5 truncate">
{[customer.first_name, customer.last_name].filter(Boolean).join(' ')} {[customer.first_name, customer.last_name].filter(Boolean).join(' ')}
{customer.first_name || customer.last_name ? ' · ' : ''} {customer.first_name || customer.last_name ? ' · ' : ''}
{[customer.street, customer.zip, customer.city].filter(Boolean).join(', ')} {[customer.street, customer.zip, customer.city].filter(Boolean).join(', ')}
</p> </p>
{customer.email && (
<p className="text-[10px] text-slate-500 mt-0.5 flex items-center gap-1 truncate">
<Mail className="w-3 h-3 shrink-0" /> {customer.email}
</p>
)}
</div> </div>
{isSelected && <Check className="w-5 h-5 text-primary mt-0.5 shrink-0" />} {isSelected && <Check className="w-4 h-4 text-primary shrink-0 mt-0.5" />}
</div> </div>
); )
})} })
)}
</div> </div>
{/* Customer Pagination Footer */} {/* Customer Pagination */}
<div className="flex items-center justify-between border-t border-white/10 pt-3 text-xs text-slate-400 flex-wrap gap-2"> {totalCustomerCount > 0 && (
<div> <div className="flex items-center justify-between border-t border-white/10 pt-2 text-[11px] text-slate-400 shrink-0 flex-wrap gap-2">
Zeige {totalCustomerCount === 0 ? 0 : (currentPage - 1) * pageSize + 1} bis{' '} <div className="flex items-center gap-2">
{Math.min(currentPage * pageSize, totalCustomerCount)} von {totalCustomerCount} Kunden <span>
{Math.min(currentPage * pageSize, totalCustomerCount)} / {totalCustomerCount} Kunden
</span>
<select
value={pageSize}
onChange={(e) => setPageSize(Number(e.target.value))}
className="h-6 rounded border border-white/10 bg-slate-900 px-1 text-[11px] text-white focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary"
>
<option value={10}>10 pro Seite</option>
<option value={20}>20 pro Seite</option>
<option value={50}>50 pro Seite</option>
</select>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@@ -506,127 +516,362 @@ export function StepCustomer({
size="sm" size="sm"
onClick={() => setCurrentPage(1)} onClick={() => setCurrentPage(1)}
disabled={currentPage === 1} disabled={currentPage === 1}
className="h-8 px-2 text-slate-400 hover:text-white" className="h-6 px-1.5 text-slate-400 hover:text-white"
title="Erste Seite"
> >
<ChevronsLeft className="w-4 h-4" /> <ChevronsLeft className="w-3.5 h-3.5" />
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage === 1} disabled={currentPage === 1}
className="h-8 px-2.5 border-white/10 text-white" className="h-6 px-2 border-white/10 text-white text-[11px]"
> >
<ChevronLeft className="w-3.5 h-3.5 mr-1" /> Zurück <ChevronLeft className="w-3 h-3 mr-0.5" /> Zurück
</Button> </Button>
<span className="px-1.5 text-slate-300">
<span className="px-2 font-medium text-slate-300"> {currentPage} / {totalCustomerPages}
Seite {currentPage} von {totalCustomerPages}
</span> </span>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage((p) => Math.min(totalCustomerPages, p + 1))} onClick={() => setCurrentPage((p) => Math.min(totalCustomerPages, p + 1))}
disabled={currentPage === totalCustomerPages} disabled={currentPage === totalCustomerPages}
className="h-8 px-2.5 border-white/10 text-white" className="h-6 px-2 border-white/10 text-white text-[11px]"
> >
Vor <ChevronRight className="w-3.5 h-3.5 ml-1" /> Vor <ChevronRight className="w-3 h-3 ml-0.5" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => setCurrentPage(totalCustomerPages)} onClick={() => setCurrentPage(totalCustomerPages)}
disabled={currentPage === totalCustomerPages} disabled={currentPage === totalCustomerPages}
className="h-8 px-2 text-slate-400 hover:text-white" className="h-8 px-1.5 text-slate-400 hover:text-white"
title="Letzte Seite"
> >
<ChevronsRight className="w-4 h-4" /> <ChevronsRight className="w-3.5 h-3.5" />
</Button> </Button>
</div> </div>
</div> </div>
</div>
)} )}
</div> </div>
)} </div>
</motion.div> </div>
)}
{/* ─── MODUS B: NEUEN KUNDEN ANLEGEN ─── */} {/* ─── 3. FIXED BOTTOM ACTION BAR (shrink-0) ─── */}
{customerMode === 'create' && ( <div className="shrink-0 border-t border-white/10 bg-slate-950/90 backdrop-blur-md px-6 py-3.5 flex flex-col sm:flex-row items-center justify-between gap-3 z-10">
<motion.div {/* Selected Customer Summary Chip */}
key="create" <div className="flex items-center gap-2.5 min-w-0">
initial={{ opacity: 0, y: 10 }} {activeCustomer ? (
animate={{ opacity: 1, y: 0 }} <div className="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-primary/10 border border-primary/30 text-white min-w-0">
exit={{ opacity: 0, y: -10 }} <CheckCircle2 className="w-4 h-4 text-primary shrink-0" />
transition={{ duration: 0.2 }} <div className="text-xs truncate">
className="grid grid-cols-1 md:grid-cols-2 gap-4 p-5 rounded-2xl bg-white/5 border border-white/10 h-[34rem] overflow-y-auto content-start" <span className="text-slate-400">Ausgewählt:</span>{' '}
> <strong className="text-white font-semibold">{activeCustomer.company_name}</strong>
{( {(activeCustomer.city || activeCustomer.zip) && (
[ <span className="text-slate-400 ml-1.5">
{ label: 'Firmenname *', key: 'company_name', span: true, placeholder: 'GmbH / Einzelunternehmen' }, ({[activeCustomer.zip, activeCustomer.city].filter(Boolean).join(' ')})
{ label: 'USt-IdNr.', key: 'vat_id', span: false, placeholder: 'DE123456789' }, </span>
{ label: 'Vorname', key: 'first_name', span: false, placeholder: '' },
{ label: 'Nachname', key: 'last_name', span: false, placeholder: '' },
{ label: 'Straße & Hausnummer', key: 'street', span: true, placeholder: '' },
{ label: 'PLZ', key: 'zip', span: false, placeholder: '' },
{ label: 'Ort', key: 'city', span: false, placeholder: '' },
{ label: 'Kontoinhaber', key: 'bank_owner', span: false, placeholder: 'Max Mustermann' },
{ label: 'IBAN', key: 'bank_iban', span: false, placeholder: 'DE89370400440532013000' },
{ label: 'BIC', key: 'bank_bic', span: false, placeholder: 'SOLADE21XXX' },
{ label: 'Bankname', key: 'bank_name', span: false, placeholder: 'Musterbank' },
] as const
).map(({ label, key, span, placeholder }) => (
<div key={key} className={`space-y-1.5 ${span ? 'md:col-span-2' : ''}`}>
<Label className="text-slate-300 text-sm">{label}</Label>
<Input
value={newCustomerForm[key] || ''}
onChange={(e) =>
setNewCustomerForm((prev: any) => ({ ...prev, [key]: e.target.value }))
}
placeholder={placeholder}
className="bg-white/5 border-white/10 text-white placeholder:text-slate-500"
/>
</div>
))}
<div className="md:col-span-2 flex gap-2">
<Button
onClick={handleCreateCustomer}
disabled={isCreatingCustomer || !newCustomerForm.company_name?.trim()}
className="gap-2"
>
{isCreatingCustomer ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<UserPlus className="w-4 h-4" />
)} )}
Kunden speichern & auswählen </div>
</div>
) : (
<div className="text-xs text-slate-400 flex items-center gap-1.5">
<User className="w-4 h-4 text-slate-500" />
<span>Bitte wählen Sie einen Kunden aus, um fortzufahren.</span>
</div>
)}
</div>
{/* Next Button */}
<Button
onClick={nextStep}
disabled={!selectedEndCustomerId}
className="w-full sm:w-auto bg-primary hover:bg-primary/90 text-white font-semibold px-6 shadow-lg shadow-primary/20 h-9 text-xs gap-1.5"
>
Weiter zu Schritt 2 <ChevronRight className="w-4 h-4" />
</Button> </Button>
</div>
</Card>
{/* ─── 4. SLIDE-OVER DRAWER FOR CUSTOMER CREATION ─── */}
<AnimatePresence>
{isDrawerOpen && (
<div className="fixed inset-0 z-50 overflow-hidden">
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={closeDrawer}
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
/>
<div className="fixed inset-y-0 right-0 max-w-full flex pl-10">
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
className="w-screen max-w-xl bg-slate-900 border-l border-white/10 shadow-2xl flex flex-col justify-between"
>
{/* Drawer Header */}
<div className="p-5 border-b border-white/10 flex items-center justify-between bg-slate-950/60 shrink-0">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-primary/20 border border-primary/30 flex items-center justify-center text-primary">
<UserPlus className="w-4 h-4" />
</div>
<div>
<h3 className="text-base font-bold text-white">Neuen Kunden anlegen</h3>
<p className="text-xs text-slate-400">
{isAdmin && selectedCompanyId && selectedCompanyId !== 'all'
? `Für Partner: ${companies.find((c) => c.id === selectedCompanyId)?.name || selectedCompanyId}`
: 'Erstellen und direkt für die Bestellung auswählen'}
</p>
</div>
</div>
<Button <Button
variant="ghost" variant="ghost"
className="text-white" size="icon"
onClick={() => setCustomerMode('select')} onClick={closeDrawer}
className="text-slate-400 hover:text-white rounded-full h-8 w-8"
> >
Abbrechen <X className="w-4 h-4" />
</Button> </Button>
</div> </div>
{/* Drawer Body / Form */}
<form id="new-customer-form" onSubmit={onSubmitCustomerForm} className="p-5 overflow-y-auto flex-1 space-y-5 scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
{/* Basic Info Section */}
<div className="space-y-3.5">
<div className="flex items-center gap-2 pb-1 border-b border-white/5">
<Building className="w-3.5 h-3.5 text-primary" />
<span className="text-xs font-bold uppercase tracking-wider text-slate-300">
Stammdaten & Anschrift
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3.5">
<div className="md:col-span-2 space-y-1">
<Label className="text-slate-300 text-xs font-semibold">Firmenname *</Label>
<Input
required
value={newCustomerForm.company_name || ''}
onChange={(e) =>
setNewCustomerForm((prev: any) => ({ ...prev, company_name: e.target.value }))
}
placeholder="z. B. Musterfirma GmbH & Co. KG"
className="bg-slate-950/70 border-white/10 text-white placeholder:text-slate-500 focus:border-primary text-xs h-8"
/>
</div>
<div className="space-y-1">
<Label className="text-slate-300 text-xs font-semibold">USt-IdNr.</Label>
<Input
value={newCustomerForm.vat_id || ''}
onChange={(e) =>
setNewCustomerForm((prev: any) => ({ ...prev, vat_id: e.target.value }))
}
placeholder="z. B. DE123456789"
className="bg-slate-950/70 border-white/10 text-white placeholder:text-slate-500 focus:border-primary text-xs h-8"
/>
</div>
<div className="space-y-1">
<Label className="text-slate-300 text-xs font-semibold">E-Mail</Label>
<Input
type="email"
value={newCustomerForm.email || ''}
onChange={(e) =>
setNewCustomerForm((prev: any) => ({ ...prev, email: e.target.value }))
}
placeholder="buchhaltung@muster.de"
className="bg-slate-950/70 border-white/10 text-white placeholder:text-slate-500 focus:border-primary text-xs h-8"
/>
</div>
<div className="space-y-1">
<Label className="text-slate-300 text-xs font-semibold">Vorname</Label>
<Input
value={newCustomerForm.first_name || ''}
onChange={(e) =>
setNewCustomerForm((prev: any) => ({ ...prev, first_name: e.target.value }))
}
placeholder="Max"
className="bg-slate-950/70 border-white/10 text-white placeholder:text-slate-500 focus:border-primary text-xs h-8"
/>
</div>
<div className="space-y-1">
<Label className="text-slate-300 text-xs font-semibold">Nachname</Label>
<Input
value={newCustomerForm.last_name || ''}
onChange={(e) =>
setNewCustomerForm((prev: any) => ({ ...prev, last_name: e.target.value }))
}
placeholder="Mustermann"
className="bg-slate-950/70 border-white/10 text-white placeholder:text-slate-500 focus:border-primary text-xs h-8"
/>
</div>
<div className="md:col-span-2 space-y-1">
<Label className="text-slate-300 text-xs font-semibold">Straße & Hausnummer</Label>
<Input
value={newCustomerForm.street || ''}
onChange={(e) =>
setNewCustomerForm((prev: any) => ({ ...prev, street: e.target.value }))
}
placeholder="Hauptstraße 12"
className="bg-slate-950/70 border-white/10 text-white placeholder:text-slate-500 focus:border-primary text-xs h-8"
/>
</div>
<div className="space-y-1">
<Label className="text-slate-300 text-xs font-semibold">PLZ</Label>
<Input
value={newCustomerForm.zip || ''}
onChange={(e) =>
setNewCustomerForm((prev: any) => ({ ...prev, zip: e.target.value }))
}
placeholder="10115"
className="bg-slate-950/70 border-white/10 text-white placeholder:text-slate-500 focus:border-primary text-xs h-8"
/>
</div>
<div className="space-y-1">
<Label className="text-slate-300 text-xs font-semibold">Ort</Label>
<Input
value={newCustomerForm.city || ''}
onChange={(e) =>
setNewCustomerForm((prev: any) => ({ ...prev, city: e.target.value }))
}
placeholder="Berlin"
className="bg-slate-950/70 border-white/10 text-white placeholder:text-slate-500 focus:border-primary text-xs h-8"
/>
</div>
</div>
</div>
{/* Collapsible Bank Details Section */}
<div className="rounded-xl border border-white/10 bg-slate-950/40 overflow-hidden">
<button
type="button"
onClick={() => setShowBankDetails((prev) => !prev)}
className="w-full flex items-center justify-between p-3 text-left hover:bg-white/5 transition-colors"
>
<div className="flex items-center gap-2">
<CreditCard className="w-3.5 h-3.5 text-slate-400" />
<div>
<p className="text-xs font-bold text-white">Optionale Zahlungs-/Bankdaten</p>
<p className="text-[10px] text-slate-400">SEPA-Lastschrift, IBAN & BIC hinterlegen</p>
</div>
</div>
{showBankDetails ? (
<ChevronUp className="w-3.5 h-3.5 text-slate-400" />
) : (
<ChevronDown className="w-3.5 h-3.5 text-slate-400" />
)}
</button>
<AnimatePresence>
{showBankDetails && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="p-3.5 border-t border-white/10 space-y-3 bg-white/[0.02]"
>
<div className="space-y-1">
<Label className="text-slate-300 text-xs">Kontoinhaber</Label>
<Input
value={newCustomerForm.bank_owner || ''}
onChange={(e) =>
setNewCustomerForm((prev: any) => ({ ...prev, bank_owner: e.target.value }))
}
placeholder="Max Mustermann"
className="bg-slate-950/70 border-white/10 text-white placeholder:text-slate-500 focus:border-primary text-xs h-8"
/>
</div>
<div className="space-y-1">
<Label className="text-slate-300 text-xs">IBAN</Label>
<Input
value={newCustomerForm.bank_iban || ''}
onChange={(e) =>
setNewCustomerForm((prev: any) => ({ ...prev, bank_iban: e.target.value }))
}
placeholder="DE89370400440532013000"
className="bg-slate-950/70 border-white/10 text-white placeholder:text-slate-500 focus:border-primary text-xs h-8 font-mono"
/>
</div>
<div className="grid grid-cols-2 gap-2.5">
<div className="space-y-1">
<Label className="text-slate-300 text-xs">BIC</Label>
<Input
value={newCustomerForm.bank_bic || ''}
onChange={(e) =>
setNewCustomerForm((prev: any) => ({ ...prev, bank_bic: e.target.value }))
}
placeholder="SOLADE21XXX"
className="bg-slate-950/70 border-white/10 text-white placeholder:text-slate-500 focus:border-primary text-xs h-8 font-mono"
/>
</div>
<div className="space-y-1">
<Label className="text-slate-300 text-xs">Bankname</Label>
<Input
value={newCustomerForm.bank_name || ''}
onChange={(e) =>
setNewCustomerForm((prev: any) => ({ ...prev, bank_name: e.target.value }))
}
placeholder="Musterbank"
className="bg-slate-950/70 border-white/10 text-white placeholder:text-slate-500 focus:border-primary text-xs h-8"
/>
</div>
</div>
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
</CardContent> </form>
<CardFooter className="flex justify-end border-t border-white/10 pt-6">
{/* Drawer Footer */}
<div className="p-4 border-t border-white/10 bg-slate-950/60 flex items-center justify-end gap-2.5 shrink-0">
<Button <Button
onClick={nextStep} type="button"
disabled={ variant="ghost"
customerMode === 'create' || onClick={closeDrawer}
!selectedEndCustomerId disabled={isCreatingCustomer}
} className="text-slate-300 hover:text-white h-8 text-xs"
> >
Weiter zum Abrechnungsmodell <ChevronRight className="ml-2 w-4 h-4" /> Abbrechen
</Button> </Button>
</CardFooter> <Button
</Card> type="submit"
form="new-customer-form"
disabled={isCreatingCustomer || !newCustomerForm.company_name?.trim()}
className="gap-2 bg-primary hover:bg-primary/90 text-white font-semibold px-4 h-8 text-xs"
>
{isCreatingCustomer ? (
<>
<Loader2 className="w-3.5 h-3.5 animate-spin" />
Speichere Kunde...
</>
) : (
<>
<UserPlus className="w-3.5 h-3.5" />
Kunden anlegen & auswählen
</>
)}
</Button>
</div>
</motion.div>
</div>
</div>
)}
</AnimatePresence>
</>
) )
} }

View File

@@ -2,13 +2,13 @@
import React from 'react' import React from 'react'
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card' import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { ShoppingCart, Check, AlertCircle, Lock } from 'lucide-react' import { Button } from '@/components/ui/button'
import { ShoppingCart, Check, AlertCircle, Lock, X, Info } from 'lucide-react'
import * as Icons from 'lucide-react' import * as Icons from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion' import { motion, AnimatePresence } from 'framer-motion'
import { Category, Product, CategorySelection } from '@/lib/types' import { Category, Product, CategorySelection } from '@/lib/types'
@@ -30,6 +30,8 @@ interface StepSoftwareProps {
existingModuleIds?: string[] existingModuleIds?: string[]
activeCategoryId: string | null activeCategoryId: string | null
editingDeviceName?: string | null editingDeviceName?: string | null
onCancelEdit?: () => void
productValidationErrors?: string[]
} }
export function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) { export function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) {
@@ -54,6 +56,8 @@ export function StepSoftware({
existingModuleIds = [], existingModuleIds = [],
activeCategoryId, activeCategoryId,
editingDeviceName = null, editingDeviceName = null,
onCancelEdit,
productValidationErrors = [],
}: StepSoftwareProps) { }: StepSoftwareProps) {
const currentCategory = visibleCategories.find(c => c.id === activeCategoryId) ?? visibleCategories[0] ?? null const currentCategory = visibleCategories.find(c => c.id === activeCategoryId) ?? visibleCategories[0] ?? null
@@ -69,24 +73,46 @@ export function StepSoftware({
const selectedProduct = catProducts.find(p => p.id === sel?.productId) ?? null const selectedProduct = catProducts.find(p => p.id === sel?.productId) ?? null
return ( return (
<Card className="glass-dark border-white/10 h-full flex flex-col rounded-2xl"> <Card className="glass-dark border-white/10 h-full flex flex-col rounded-2xl overflow-hidden">
<CardHeader className="border-b border-white/5 pb-4"> {/* ─── FIXED CARD HEADER ─── */}
<CardTitle className="text-xl flex items-center justify-between gap-2 text-white font-bold flex-wrap"> <CardHeader className="border-b border-white/10 pb-3 pt-4 px-5 shrink-0 bg-slate-950/40">
<div className="flex items-center gap-2"> <div className="flex items-center justify-between gap-3 flex-wrap">
<ShoppingCart className="w-5 h-5 text-primary" /> <div className="flex items-center gap-2.5">
<span>Optionen wählen</span> <div className="p-2 rounded-xl bg-primary/20 text-primary border border-primary/30">
<ShoppingCart className="w-4 h-4" />
</div> </div>
<div>
<CardTitle className="text-base font-bold text-white">
{currentCategory.name}
</CardTitle>
<CardDescription className="text-xs text-slate-400 mt-0.5">
{currentCategory.description || 'Passen Sie die Konfiguration für diese Kategorie an.'}
</CardDescription>
</div>
</div>
{editingDeviceName && ( {editingDeviceName && (
<Badge className="bg-amber-500/20 text-amber-400 border border-amber-500/30 text-xs font-bold gap-1 px-2.5 py-1 rounded-lg animate-pulse shrink-0"> <div className="flex items-center gap-2">
<Badge className="bg-amber-500/20 text-amber-400 border border-amber-500/30 text-xs font-bold gap-1.5 px-3 py-1 rounded-lg animate-pulse shrink-0">
Bearbeite Kasse: &quot;{editingDeviceName}&quot; Bearbeite Kasse: &quot;{editingDeviceName}&quot;
</Badge> </Badge>
{onCancelEdit && (
<Button
size="sm"
variant="ghost"
onClick={onCancelEdit}
className="h-7 text-xs text-slate-400 hover:text-white hover:bg-white/10 px-2"
>
<X className="w-3.5 h-3.5 mr-1" /> Neuer Entwurf
</Button>
)} )}
</CardTitle> </div>
<CardDescription className="text-slate-400"> )}
Passen Sie die Konfiguration für {currentCategory.name} an. </div>
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex-1 pt-6 space-y-6">
{/* ─── CONTENT AREA (Inherits scrolling from parent, no nested double scrollbar) ─── */}
<CardContent className="p-5 space-y-5">
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
<motion.div <motion.div
key={currentCategory.id} key={currentCategory.id}
@@ -94,49 +120,49 @@ export function StepSoftware({
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }} exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className="space-y-6" className="space-y-5"
> >
{/* Category header */} {/* Category selection status header */}
{(() => { {(() => {
const isMissingRequired = currentCategory.is_required && !sel?.productId && (!sel?.productIds || sel.productIds.length === 0) const isMissingRequired = currentCategory.is_required && !sel?.productId && (!sel?.productIds || sel.productIds.length === 0)
return ( return (
<div className={`flex items-center gap-3 p-4 rounded-xl border transition-all ${ <div className={`flex items-center gap-3 p-3.5 rounded-xl border transition-all ${
isMissingRequired isMissingRequired
? 'bg-red-500/10 border-red-500/50 text-red-400 font-bold' ? 'bg-red-500/10 border-red-500/50 text-red-400 font-bold'
: 'bg-white/5 border-white/10' : 'bg-white/5 border-white/10'
}`}> }`}>
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${ <div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${
isMissingRequired ? 'bg-red-500/20 text-red-400' : 'bg-primary/20 text-primary' isMissingRequired ? 'bg-red-500/20 text-red-400' : 'bg-primary/20 text-primary'
}`}> }`}>
<CategoryIcon icon={currentCategory.icon} className="w-4 h-4" /> <CategoryIcon icon={currentCategory.icon} className="w-4 h-4" />
</div> </div>
<div> <div className="min-w-0 flex-1">
<h3 className="font-bold text-white text-sm flex items-center gap-2"> <h3 className="font-bold text-white text-xs flex items-center gap-2 flex-wrap">
{currentCategory.name} <span>{currentCategory.name}</span>
{isMissingRequired && ( {isMissingRequired && (
<span className="text-[10px] bg-red-500/20 text-red-400 border border-red-500/40 px-2 py-0.5 rounded-full font-extrabold animate-pulse tracking-wider"> <span className="text-[10px] bg-red-500/20 text-red-400 border border-red-500/40 px-2 py-0.5 rounded-full font-extrabold animate-pulse tracking-wider">
PFLICHTFELD AUSWÄHLEN! PFLICHTFELD AUSWÄHLEN!
</span> </span>
)} )}
</h3> </h3>
{currentCategory.description && ( <p className="text-slate-400 text-[11px] mt-0.5">
<p className="text-slate-400 text-xs mt-0.5">{currentCategory.description}</p> {currentCategory.allow_multiselect ? 'Mehrfachauswahl möglich' : 'Einzelauswahl'}
)} </p>
</div> </div>
{sel?.productIds && sel.productIds.length > 0 ? ( {sel?.productIds && sel.productIds.length > 0 ? (
<Badge className="ml-auto bg-green-500/20 text-green-400 border border-green-500/30 text-xs rounded-full"> <Badge className="ml-auto bg-green-500/20 text-green-400 border border-green-500/30 text-xs rounded-full shrink-0">
<Check className="w-3 h-3 mr-1" /> {sel.productIds.length} Ausgewählt <Check className="w-3 h-3 mr-1" /> {sel.productIds.length} Ausgewählt
</Badge> </Badge>
) : sel?.productId ? ( ) : sel?.productId ? (
<Badge className="ml-auto bg-green-500/20 text-green-400 border border-green-500/30 text-xs rounded-full"> <Badge className="ml-auto bg-green-500/20 text-green-400 border border-green-500/30 text-xs rounded-full shrink-0">
<Check className="w-3 h-3 mr-1" /> Ausgewählt <Check className="w-3 h-3 mr-1" /> Ausgewählt
</Badge> </Badge>
) : currentCategory.is_required ? ( ) : currentCategory.is_required ? (
<Badge variant="destructive" className="ml-auto opacity-90 text-xs rounded-full bg-red-500/30 text-red-400 border border-red-500/50 animate-pulse font-extrabold"> <Badge variant="destructive" className="ml-auto opacity-90 text-xs rounded-full bg-red-500/30 text-red-400 border border-red-500/50 animate-pulse font-extrabold shrink-0">
<AlertCircle className="w-3 h-3 mr-1 text-red-400" /> Bitte auswählen! <AlertCircle className="w-3 h-3 mr-1 text-red-400" /> Bitte auswählen!
</Badge> </Badge>
) : ( ) : (
<Badge variant="outline" className="ml-auto border-white/10 text-slate-400 text-xs rounded-full bg-white/5"> <Badge variant="outline" className="ml-auto border-white/10 text-slate-400 text-xs rounded-full bg-white/5 shrink-0">
Optional Optional
</Badge> </Badge>
)} )}
@@ -144,12 +170,13 @@ export function StepSoftware({
) )
})()} })()}
{/* Products List */}
{catProducts.length === 0 ? ( {catProducts.length === 0 ? (
<p className="text-slate-500 text-sm italic"> <p className="text-slate-500 text-xs italic p-4 bg-white/5 rounded-xl border border-white/5 text-center">
Keine Produkte in dieser Kategorie im aktuellen Abrechnungsmodell vorhanden. Keine Produkte in dieser Kategorie im aktuellen Abrechnungsmodell vorhanden.
</p> </p>
) : currentCategory.allow_multiselect ? ( ) : currentCategory.allow_multiselect ? (
<div className="grid gap-3"> <div className="grid gap-2.5">
{catProducts.map(product => { {catProducts.map(product => {
const isChecked = sel?.productIds?.includes(product.id) ?? false const isChecked = sel?.productIds?.includes(product.id) ?? false
const disabled = isProductDisabled(product, currentCategory.id) const disabled = isProductDisabled(product, currentCategory.id)
@@ -157,27 +184,27 @@ export function StepSoftware({
<div key={product.id} className="relative"> <div key={product.id} className="relative">
<Label <Label
onClick={() => !disabled && selectProduct(currentCategory.id, product.id)} onClick={() => !disabled && selectProduct(currentCategory.id, product.id)}
className={`flex flex-col items-start p-4 rounded-xl border transition-all ${disabled className={`flex flex-col items-start p-3.5 rounded-xl border transition-all ${disabled
? 'border-white/5 bg-white/5 opacity-40 cursor-not-allowed' ? 'border-white/5 bg-white/5 opacity-40 cursor-not-allowed'
: isChecked : isChecked
? 'border-primary bg-primary/10 cursor-pointer shadow-[0_0_15px_rgba(59,130,246,0.1)]' ? 'border-primary bg-primary/10 cursor-pointer shadow-[0_0_15px_rgba(59,130,246,0.15)] ring-1 ring-primary/40'
: 'border-white/5 bg-white/5 hover:bg-white/10 cursor-pointer' : 'border-white/5 bg-white/5 hover:bg-white/10 cursor-pointer'
}`} }`}
> >
<div className="flex justify-between w-full items-center"> <div className="flex justify-between w-full items-center">
<div className="flex items-center gap-3"> <div className="flex items-center gap-2.5">
<Checkbox <Checkbox
checked={isChecked} checked={isChecked}
disabled={disabled} disabled={disabled}
onCheckedChange={() => { }} onCheckedChange={() => { }}
className="border-white/20 data-[state=checked]:bg-primary rounded" className="border-white/20 data-[state=checked]:bg-primary rounded"
/> />
<span className="font-bold text-sm text-white">{product.name}</span> <span className="font-bold text-xs text-white">{product.name}</span>
<span className={`text-[9px] px-1.5 py-0.5 rounded border ${billingBadgeClass(product.billing_interval)} font-medium`}> <span className={`text-[9px] px-1.5 py-0.5 rounded border ${billingBadgeClass(product.billing_interval)} font-medium`}>
{product.billing_interval === 'one_time' ? 'Einmalig' : 'Abo'} {product.billing_interval === 'one_time' ? 'Einmalig' : 'Abo'}
</span> </span>
</div> </div>
<span className="text-primary font-bold text-sm"> <span className="text-primary font-bold text-xs">
{new Intl.NumberFormat('de-DE', { {new Intl.NumberFormat('de-DE', {
style: 'currency', style: 'currency',
currency: 'EUR', currency: 'EUR',
@@ -186,10 +213,10 @@ export function StepSoftware({
</span> </span>
</div> </div>
{product.description && ( {product.description && (
<span className="text-xs text-slate-400 mt-2 pl-7 font-normal leading-relaxed">{product.description}</span> <span className="text-[11px] text-slate-400 mt-1.5 pl-6 font-normal leading-relaxed">{product.description}</span>
)} )}
{product.modules && product.modules.length > 0 && ( {product.modules && product.modules.length > 0 && (
<span className="text-[10px] text-slate-500 mt-1 pl-7 font-normal"> <span className="text-[10px] text-slate-500 mt-1 pl-6 font-normal">
{product.modules.length} optionale Module verfügbar {product.modules.length} optionale Module verfügbar
</span> </span>
)} )}
@@ -202,7 +229,7 @@ export function StepSoftware({
<RadioGroup <RadioGroup
value={sel?.productId ?? ''} value={sel?.productId ?? ''}
onValueChange={id => selectProduct(currentCategory.id, id)} onValueChange={id => selectProduct(currentCategory.id, id)}
className="grid gap-3" className="grid gap-2.5"
> >
{catProducts.map(product => { {catProducts.map(product => {
const disabled = isProductDisabled(product, currentCategory.id) const disabled = isProductDisabled(product, currentCategory.id)
@@ -217,21 +244,21 @@ export function StepSoftware({
/> />
<Label <Label
htmlFor={disabled ? undefined : `${currentCategory.id}-${product.id}`} htmlFor={disabled ? undefined : `${currentCategory.id}-${product.id}`}
className={`flex flex-col items-start p-4 rounded-xl border transition-all ${disabled className={`flex flex-col items-start p-3.5 rounded-xl border transition-all ${disabled
? 'border-white/5 bg-white/5 opacity-40 cursor-not-allowed' ? 'border-white/5 bg-white/5 opacity-40 cursor-not-allowed'
: isChecked : isChecked
? 'border-primary bg-primary/10 cursor-pointer shadow-[0_0_15px_rgba(59,130,246,0.1)]' ? 'border-primary bg-primary/10 cursor-pointer shadow-[0_0_15px_rgba(59,130,246,0.15)] ring-1 ring-primary/40'
: 'border-white/5 bg-white/5 hover:bg-white/10 cursor-pointer' : 'border-white/5 bg-white/5 hover:bg-white/10 cursor-pointer'
}`} }`}
> >
<div className="flex justify-between w-full items-center"> <div className="flex justify-between w-full items-center">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2.5">
<span className="font-bold text-sm text-white">{product.name}</span> <span className="font-bold text-xs text-white">{product.name}</span>
<span className={`text-[9px] px-1.5 py-0.5 rounded border ${billingBadgeClass(product.billing_interval)} font-medium`}> <span className={`text-[9px] px-1.5 py-0.5 rounded border ${billingBadgeClass(product.billing_interval)} font-medium`}>
{product.billing_interval === 'one_time' ? 'Einmalig' : 'Abo'} {product.billing_interval === 'one_time' ? 'Einmalig' : 'Abo'}
</span> </span>
</div> </div>
<span className="text-primary font-bold text-sm"> <span className="text-primary font-bold text-xs">
{new Intl.NumberFormat('de-DE', { {new Intl.NumberFormat('de-DE', {
style: 'currency', style: 'currency',
currency: 'EUR', currency: 'EUR',
@@ -240,7 +267,7 @@ export function StepSoftware({
</span> </span>
</div> </div>
{product.description && ( {product.description && (
<span className="text-xs text-slate-400 mt-2 font-normal leading-relaxed">{product.description}</span> <span className="text-[11px] text-slate-400 mt-1.5 font-normal leading-relaxed">{product.description}</span>
)} )}
{product.modules && product.modules.length > 0 && ( {product.modules && product.modules.length > 0 && (
<span className="text-[10px] text-slate-500 mt-1 font-normal"> <span className="text-[10px] text-slate-500 mt-1 font-normal">
@@ -254,14 +281,14 @@ export function StepSoftware({
</RadioGroup> </RadioGroup>
)} )}
{/* Modules */} {/* Modules Section */}
{selectedProduct?.modules && selectedProduct.modules.length > 0 && ( {selectedProduct?.modules && selectedProduct.modules.length > 0 && (
<div className="mt-6 p-5 rounded-2xl bg-white/5 border border-white/10 space-y-4"> <div className="mt-5 p-4 rounded-2xl bg-white/5 border border-white/10 space-y-3.5">
<p className="text-sm font-bold text-white flex items-center gap-2"> <p className="text-xs font-bold text-white flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-primary" /> <span className="w-1.5 h-1.5 rounded-full bg-primary" />
Zusatzmodule für {selectedProduct.name} Zusatzmodule für {selectedProduct.name}
</p> </p>
<div className="space-y-2.5"> <div className="space-y-2">
{selectedProduct.modules.map(module => { {selectedProduct.modules.map(module => {
const isExistingLicense = existingModuleIds.includes(module.id) const isExistingLicense = existingModuleIds.includes(module.id)
const disabled = isExistingLicense || isModuleDisabled(module, sel?.moduleIds ?? []) const disabled = isExistingLicense || isModuleDisabled(module, sel?.moduleIds ?? [])
@@ -270,13 +297,13 @@ export function StepSoftware({
<div <div
key={module.id} key={module.id}
title={isExistingLicense ? "Dieses Modul ist auf dieser Kasse bereits aktiv und dauerhaft lizenziert." : undefined} title={isExistingLicense ? "Dieses Modul ist auf dieser Kasse bereits aktiv und dauerhaft lizenziert." : undefined}
className={`flex flex-col p-4 rounded-xl border transition-all duration-200 ${ className={`flex flex-col p-3.5 rounded-xl border transition-all duration-200 ${
isExistingLicense isExistingLicense
? 'border-primary/20 bg-primary/5 opacity-75' ? 'border-primary/20 bg-primary/5 opacity-75'
: disabled : disabled
? 'border-white/5 bg-white/5 opacity-40' ? 'border-white/5 bg-white/5 opacity-40'
: checked : checked
? 'border-primary bg-primary/10 shadow-[0_0_15px_rgba(59,130,246,0.15)] text-white' ? 'border-primary bg-primary/10 shadow-[0_0_15px_rgba(59,130,246,0.15)] ring-1 ring-primary/40 text-white'
: 'border-white/5 bg-white/5 hover:bg-white/10' : 'border-white/5 bg-white/5 hover:bg-white/10'
}`} }`}
> >
@@ -286,12 +313,12 @@ export function StepSoftware({
checked={checked} checked={checked}
onCheckedChange={() => !isExistingLicense && toggleModule(currentCategory.id, module.id)} onCheckedChange={() => !isExistingLicense && toggleModule(currentCategory.id, module.id)}
disabled={disabled} disabled={disabled}
className="rounded border-white/20 data-[state=checked]:bg-primary" className="rounded border-white/20 data-[state=checked]:bg-primary mt-0.5"
/> />
<div className="flex-1"> <div className="flex-1 min-w-0">
<Label <Label
htmlFor={isExistingLicense ? undefined : `mod-${currentCategory.id}-${module.id}`} htmlFor={isExistingLicense ? undefined : `mod-${currentCategory.id}-${module.id}`}
className={`font-semibold text-sm flex justify-between text-white ${ className={`font-semibold text-xs flex justify-between text-white ${
isExistingLicense ? 'cursor-default' : disabled ? 'cursor-not-allowed' : 'cursor-pointer' isExistingLicense ? 'cursor-default' : disabled ? 'cursor-not-allowed' : 'cursor-pointer'
}`} }`}
> >
@@ -303,9 +330,9 @@ export function StepSoftware({
</span> </span>
)} )}
</span> </span>
<span className="text-primary font-bold text-sm"> <span className="text-primary font-bold text-xs ml-2 shrink-0">
{isExistingLicense ? ( {isExistingLicense ? (
<span className="text-slate-500 text-xs font-normal">inkl.</span> <span className="text-slate-500 text-[11px] font-normal">inkl.</span>
) : ( ) : (
<>+{new Intl.NumberFormat('de-DE', { <>+{new Intl.NumberFormat('de-DE', {
style: 'currency', style: 'currency',
@@ -315,10 +342,10 @@ export function StepSoftware({
</span> </span>
</Label> </Label>
{module.description && ( {module.description && (
<p className="text-xs text-slate-400 mt-1 leading-relaxed">{module.description}</p> <p className="text-[11px] text-slate-400 mt-1 leading-relaxed">{module.description}</p>
)} )}
{!isExistingLicense && disabled && ( {!isExistingLicense && disabled && (
<p className="text-[10px] text-red-400 mt-1.5 font-medium flex items-center gap-1"> <p className="text-[10px] text-red-400 mt-1 font-medium flex items-center gap-1">
<AlertCircle className="w-3 h-3 text-red-400 shrink-0" /> <AlertCircle className="w-3 h-3 text-red-400 shrink-0" />
{module.requirements?.length && !module.requirements.some( {module.requirements?.length && !module.requirements.some(
reqId => sel?.moduleIds.includes(reqId) reqId => sel?.moduleIds.includes(reqId)
@@ -332,8 +359,8 @@ export function StepSoftware({
{/* Scalable Quantity */} {/* Scalable Quantity */}
{checked && !isExistingLicense && module.has_quantity && ( {checked && !isExistingLicense && module.has_quantity && (
<div className="flex items-center gap-3 mt-4 pl-8 pt-3 border-t border-white/5"> <div className="flex items-center gap-3 mt-3 pl-7 pt-2.5 border-t border-white/5">
<Label htmlFor={`qty-${module.id}`} className="text-xs text-slate-400 font-medium">Menge:</Label> <Label htmlFor={`qty-${module.id}`} className="text-[11px] text-slate-400 font-medium">Menge:</Label>
<Input <Input
id={`qty-${module.id}`} id={`qty-${module.id}`}
type="number" type="number"
@@ -344,9 +371,9 @@ export function StepSoftware({
const val = Math.max(1, parseInt(e.target.value) || 1) const val = Math.max(1, parseInt(e.target.value) || 1)
setModuleQuantities(prev => ({ ...prev, [module.id]: val })) setModuleQuantities(prev => ({ ...prev, [module.id]: val }))
}} }}
className="w-16 h-8 bg-white/5 border-white/10 text-white text-xs text-center rounded-lg focus:border-primary focus:ring-1 focus:ring-primary" className="w-16 h-7 bg-white/5 border-white/10 text-white text-xs text-center rounded-lg focus:border-primary focus:ring-1 focus:ring-primary"
/> />
<span className="text-xs text-slate-400"> <span className="text-[11px] text-slate-400">
Gesamt:{' '} Gesamt:{' '}
<span className="text-primary font-bold"> <span className="text-primary font-bold">
{new Intl.NumberFormat('de-DE', { {new Intl.NumberFormat('de-DE', {
@@ -363,6 +390,18 @@ export function StepSoftware({
</div> </div>
</div> </div>
)} )}
{/* Inline Validation Warnings */}
{productValidationErrors.length > 0 && (
<div className="p-3 rounded-xl bg-red-500/10 border border-red-500/30 space-y-1">
{productValidationErrors.map((err, errIdx) => (
<p key={errIdx} className="text-xs text-red-400 flex items-center gap-1.5 font-medium">
<AlertCircle className="w-3.5 h-3.5 shrink-0 text-red-400" />
{err}
</p>
))}
</div>
)}
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
</CardContent> </CardContent>

View File

@@ -1,11 +1,32 @@
'use client' 'use client'
import React from 'react' import React, { useState } from 'react'
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card' import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { ShieldCheck, Building2, Calendar, Loader2, CheckCircle2 } from 'lucide-react' import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import {
ShieldCheck,
Building2,
Calendar,
Loader2,
CheckCircle2,
Pencil,
Trash2,
Plus,
ChevronLeft,
MessageSquare,
FileText,
Lock,
Check,
X,
CreditCard,
User,
MapPin,
Send,
} from 'lucide-react'
import * as Icons from 'lucide-react' import * as Icons from 'lucide-react'
import { Category, Product, EndCustomer, Profile } from '@/lib/types' import { Category, Product, EndCustomer, Profile } from '@/lib/types'
@@ -70,150 +91,164 @@ export function StepSummary({
onAddNewBasketItem, onAddNewBasketItem,
onDeleteBasketItem, onDeleteBasketItem,
}: StepSummaryProps) { }: StepSummaryProps) {
const [confirmDeleteIdx, setConfirmDeleteIdx] = React.useState<number | null>(null) const [confirmDeleteIdx, setConfirmDeleteIdx] = useState<number | null>(null)
const [acceptedTerms, setAcceptedTerms] = useState(false)
const fmt = (val: number) => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val)
const totalPositionsCount = finalItemsToShow.length
const primaryInterval = finalItemsToShow[0]?.billingInterval || 'monthly'
return ( return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 h-full items-stretch text-left"> <Card className="glass-dark border-white/10 h-[calc(100vh-8.5rem)] flex flex-col justify-between overflow-hidden relative">
{/* LINKER BEREICH: Scrollbares Bedienfeld */} {/* ─── 1. FIXED HEADER (shrink-0) ─── */}
<div className="lg:col-span-5 flex flex-col h-full overflow-hidden pr-4 space-y-6"> <CardHeader className="pb-3 pt-4 px-6 shrink-0 border-b border-white/10 bg-slate-950/40 space-y-0">
<div className="flex-1 overflow-y-auto pr-1 space-y-6 pb-4 subpixel-antialiased scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent"> <div className="flex items-center justify-between gap-3">
{/* Header & Status Stepper */}
<div className="space-y-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 bg-primary/20 rounded-xl flex items-center justify-center border border-primary/50 shrink-0"> <div className="w-10 h-10 bg-primary/20 rounded-xl flex items-center justify-center border border-primary/40 shrink-0 text-primary">
<ShieldCheck className="w-6 h-6 text-primary" /> <ShieldCheck className="w-5 h-5" />
</div> </div>
<div> <div>
<h2 className="text-2xl font-extrabold text-white tracking-tight"> <CardTitle className="text-xl font-bold text-white flex items-center gap-2">
Anfrage prüfen Anfrage prüfen & absenden
</h2> </CardTitle>
<p className="text-slate-400 text-xs mt-0.5"> <CardDescription className="text-xs text-slate-300 mt-0.5">
Fast fertig! Bitte überprüfen Sie Ihre ausgewählte Konfiguration. Überprüfen Sie alle konfigurierten Kassen, Positionen und Konditionen vor der Freigabe.
</p> </CardDescription>
</div> </div>
</div> </div>
<Badge variant="outline" className="border-primary/30 text-primary bg-primary/10 text-xs px-3 py-1">
Schritt 4 von 4
</div>
{/* Kunden-Info Card */}
{selectedEndCustomer && (
<div className="p-4 rounded-xl bg-white/5 border border-white/10 space-y-2">
<div className="flex items-center gap-2 text-primary font-bold text-xs uppercase tracking-wider">
<Building2 className="w-4 h-4" />
<span>Ausgewählter Endkunde</span>
</div>
<p className="text-white font-bold text-sm">{selectedEndCustomer.company_name}</p>
<p className="text-slate-400 text-xs">Ansprechpartner: {[selectedEndCustomer.first_name, selectedEndCustomer.last_name].filter(Boolean).join(' ')}</p>
<p className="text-slate-400 text-xs">Adresse: {[selectedEndCustomer.street, [selectedEndCustomer.zip, selectedEndCustomer.city].filter(Boolean).join(' ')].filter(Boolean).join(', ')}</p>
</div>
)}
{/* Anmerkungen / Notizfeld */}
<div className="space-y-2 pt-2">
<label htmlFor="order-notes" className="block text-xs font-bold text-white uppercase tracking-wider">
Anmerkungen / Hinweise (optional)
</label>
<textarea
id="order-notes"
rows={4}
value={orderNotes}
onChange={e => setOrderNotes(e.target.value)}
placeholder="Besondere Hinweise, Ansprechpartner oder Terminwünsche hier eintragen..."
className="w-full p-3 rounded-xl bg-white/5 border border-white/10 text-xs text-white placeholder-slate-500 focus:border-primary focus:outline-none transition-colors duration-200 resize-none shadow-inner"
/>
</div>
<p className="text-[10px] text-slate-500 leading-relaxed">
Mit dem Klick auf {initialOrder ? 'Anfrage aktualisieren' : 'Kostenpflichtig bestellen'} akzeptieren Sie unsere AGB und die{' '}
<a href="/datenschutz" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-400">
Datenschutzerklärung
</a>.
</p>
{/* Aktions-Buttons */}
<div className="space-y-3 pt-2">
<Button
className="w-full h-12 text-sm font-bold bg-primary hover:bg-primary/90 shadow-[0_0_15px_rgba(59,130,246,0.2)] rounded-xl"
onClick={handleSubmit}
disabled={isSubmitting}
>
{isSubmitting ? (
initialOrder ? (
<><Loader2 className="mr-2 h-4 w-4 animate-spin" /> Anfrage wird aktualisiert...</>
) : (
<><Loader2 className="mr-2 h-4 w-4 animate-spin" /> Anfrage wird versendet...</>
)
) : (
initialOrder ? 'Anfrage aktualisieren' : 'Kostenpflichtig bestellen'
)}
</Button>
<Button variant="ghost" className="w-full text-slate-400 hover:text-white text-xs h-10" onClick={prevStep}>
Noch etwas ändern
</Button>
</div>
</div>
</div>
{/* RECHTER BEREICH: Eigenständig scrollbare Zusammenfassung der Anfrage */}
<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 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> </Badge>
</div> </div>
</CardHeader>
{/* ─── 2. SCROLLABLE MIDDLE (flex-1 min-h-0 overflow-y-auto) ─── */}
<div className="flex-1 min-h-0 overflow-y-auto p-6 scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 max-w-7xl mx-auto w-full items-start">
{/* ─── LINKES RASTER (lg:col-span-7) Auftragsübersicht & Kassen ─── */}
<div className="lg:col-span-7 space-y-5">
{/* Kopf-Card: Endkunde & Abrechnungsmodell */}
<div className="p-4 rounded-2xl bg-white/5 border border-white/10 space-y-3">
<div className="flex items-center justify-between gap-2 border-b border-white/5 pb-2.5 flex-wrap">
<div className="flex items-center gap-2 text-primary font-bold text-xs uppercase tracking-wider">
<Building2 className="w-4 h-4" />
<span>Kunde & Abrechnung</span>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-[10px] bg-primary/10 border-primary/30 text-primary font-semibold">
{primaryInterval === 'one_time' ? 'Einmalkauf' : 'Monatliches Abo'}
</Badge>
{lastLicenseDate && (
<Badge variant="outline" className="text-[10px] bg-emerald-500/10 border-emerald-500/30 text-emerald-400">
Stichtag: {new Date(lastLicenseDate).toLocaleDateString('de-DE')}
</Badge>
)}
</div>
</div>
{selectedEndCustomer ? (
<div className="space-y-1">
<h4 className="text-sm font-bold text-white">{selectedEndCustomer.company_name}</h4>
<div className="flex items-center gap-3 text-xs text-slate-300 flex-wrap">
{(selectedEndCustomer.first_name || selectedEndCustomer.last_name) && (
<span className="flex items-center gap-1">
<User className="w-3 h-3 text-slate-400" />
{[selectedEndCustomer.first_name, selectedEndCustomer.last_name].filter(Boolean).join(' ')}
</span>
)}
{(selectedEndCustomer.street || selectedEndCustomer.zip || selectedEndCustomer.city) && (
<span className="flex items-center gap-1">
<MapPin className="w-3 h-3 text-slate-400" />
{[selectedEndCustomer.street, [selectedEndCustomer.zip, selectedEndCustomer.city].filter(Boolean).join(' ')].filter(Boolean).join(', ')}
</span>
)}
{selectedEndCustomer.vat_id && (
<span className="text-slate-400">USt-IdNr: {selectedEndCustomer.vat_id}</span>
)}
</div>
</div>
) : (
<p className="text-xs text-slate-400 italic">Kein Endkunde zugewiesen (Partner-Bestellung).</p>
)}
</div>
{/* Kassenliste */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-xs font-bold text-slate-300 uppercase tracking-wider flex items-center gap-2">
<span>Enthaltene Kassen ({finalItemsToShow.length})</span>
</h3>
{onAddNewBasketItem && ( {onAddNewBasketItem && (
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={onAddNewBasketItem} 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" className="border-dashed border-primary/40 text-primary hover:bg-primary/10 text-xs font-semibold gap-1.5 h-7"
> >
<Icons.UserPlus className="w-3.5 h-3.5" /> + Weitere Kasse anlegen <Plus className="w-3 h-3" /> Weitere Kasse hinzufügen
</Button> </Button>
)} )}
</CardTitle> </div>
</CardHeader>
{/* Scrollbarer Content der Aufschlüsselung */} {finalItemsToShow.length === 0 ? (
<CardContent className="space-y-6 text-left flex-1 overflow-y-auto pt-6 subpixel-antialiased scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent pr-2"> <div className="p-6 rounded-2xl bg-white/5 border border-white/10 text-center space-y-2">
<div className="space-y-6"> <p className="text-xs text-slate-400 font-medium">Keine Kassen in der Aufstellung vorhanden.</p>
{finalItemsToShow.map((item, itemIdx) => ( </div>
<div key={itemIdx} className="space-y-3 border-b border-white/5 pb-5 last:border-0 last:pb-0"> ) : (
finalItemsToShow.map((item, itemIdx) => (
<div <div
onClick={() => onEditBasketItem && onEditBasketItem(itemIdx)} key={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' : ''}`} className="p-4 rounded-2xl bg-white/5 border border-white/10 space-y-3 relative overflow-hidden"
> >
<div className="flex items-center gap-2"> {/* Kassen Header Bar */}
<span className="text-white font-bold text-sm">Kasse: {item.deviceName}</span> <div className="flex justify-between items-center bg-slate-950/40 p-2.5 rounded-xl border border-white/5">
<div className="flex items-center gap-2 min-w-0">
<span className="text-white font-bold text-xs truncate">
{item.deviceName || `Kasse ${itemIdx + 1}`}
</span>
{itemIdx > 0 && (
<Badge variant="outline" className="text-[9px] px-1.5 py-0 border-emerald-500/30 text-emerald-400 bg-emerald-500/10 shrink-0">
Kasse 2+
</Badge>
)}
{item.licenseNumber && ( {item.licenseNumber && (
<span className="text-[10px] text-slate-400">({item.licenseNumber})</span> <span className="text-[10px] text-slate-400 font-mono truncate">
({item.licenseNumber})
</span>
)} )}
</div> </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`}> <div className="flex items-center gap-1.5 shrink-0">
<Badge
variant="outline"
className={`text-[9px] px-2 py-0.5 font-semibold ${
item.billingInterval === 'one_time'
? 'border-amber-500/30 text-amber-400 bg-amber-500/10'
: 'border-blue-500/30 text-blue-400 bg-blue-500/10'
}`}
>
{item.billingInterval === 'one_time' ? 'Kauf' : 'Abo'} {item.billingInterval === 'one_time' ? 'Kauf' : 'Abo'}
</span> </Badge>
{onEditBasketItem && ( {onEditBasketItem && (
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" 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" onClick={() => onEditBasketItem(itemIdx)}
className="h-7 px-2 text-xs text-slate-300 hover:text-primary hover:bg-primary/20 gap-1 rounded-lg"
title="Kasse in Schritt 3 bearbeiten"
> >
<Icons.Pencil className="w-3 h-3" /> Bearbeiten <Pencil className="w-3 h-3" /> Bearbeiten
</Button> </Button>
)} )}
{onDeleteBasketItem && ( {onDeleteBasketItem && (
<Button <Button
type="button" type="button"
variant={confirmDeleteIdx === itemIdx ? "destructive" : "ghost"} variant={confirmDeleteIdx === itemIdx ? 'destructive' : 'ghost'}
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
@@ -227,27 +262,30 @@ export function StepSummary({
onMouseLeave={() => setConfirmDeleteIdx(null)} onMouseLeave={() => setConfirmDeleteIdx(null)}
className={`h-7 px-2 text-xs transition-all ${ className={`h-7 px-2 text-xs transition-all ${
confirmDeleteIdx === itemIdx confirmDeleteIdx === itemIdx
? 'bg-red-600 text-white hover:bg-red-700 font-bold px-3 shadow-[0_0_10px_rgba(220,38,38,0.5)]' ? 'bg-red-600 text-white hover:bg-red-700 font-bold px-2.5 shadow-md shadow-red-500/30'
: 'text-slate-400 hover:text-red-400 hover:bg-red-500/20' : 'text-slate-400 hover:text-red-400 hover:bg-red-500/20'
} gap-1 rounded-lg`} } gap-1 rounded-lg`}
title="Kasse entfernen"
> >
<Icons.Trash2 className="w-3.5 h-3.5" /> <Trash2 className="w-3 h-3" />
{confirmDeleteIdx === itemIdx ? 'Wirklich löschen?' : 'Löschen'} {confirmDeleteIdx === itemIdx ? 'Wirklich löschen?' : ''}
</Button> </Button>
)} )}
</div> </div>
</div> </div>
{visibleCategories.map(cat => { {/* Enthaltene Produkte & Module */}
<div className="space-y-2 text-xs">
{visibleCategories.map((cat) => {
const sel = item.selections[cat.id] const sel = item.selections[cat.id]
const selectedProds: Product[] = [] const selectedProds: Product[] = []
if (cat.allow_multiselect && sel?.productIds) { if (cat.allow_multiselect && sel?.productIds) {
sel.productIds.forEach((pId: string) => { sel.productIds.forEach((pId: string) => {
const p = products.find(prod => prod.id === pId) const p = products.find((prod) => prod.id === pId)
if (p) selectedProds.push(p) if (p) selectedProds.push(p)
}) })
} else if (sel?.productId) { } else if (sel?.productId) {
const p = products.find(prod => prod.id === sel.productId) const p = products.find((prod) => prod.id === sel.productId)
if (p) selectedProds.push(p) if (p) selectedProds.push(p)
} }
@@ -257,33 +295,40 @@ export function StepSummary({
const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0 const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0
return ( return (
<div key={cat.id} className="text-sm space-y-2 pl-2"> <div key={cat.id} className="space-y-1 pl-1">
<span className="text-slate-500 text-[10px] font-bold uppercase tracking-wider block">{cat.name}:</span> <span className="text-slate-400 text-[10px] font-bold uppercase tracking-wider block">
{cat.name}:
</span>
{sortedProds.map((prod, idx) => { {sortedProds.map((prod, idx) => {
const isFree = idx < freeLimit const isFree = idx < freeLimit
const isAbo = item.billingInterval === 'monthly' const isAbo = item.billingInterval === 'monthly'
const displayPrice = isFree ? 0 : (isAbo ? ((prod as any).monthly_price ?? prod.base_price) : prod.base_price) const displayPrice = isFree ? 0 : isAbo ? ((prod as any).monthly_price ?? prod.base_price) : prod.base_price
return ( return (
<div key={prod.id} className="flex justify-between text-slate-200"> <div key={prod.id} className="flex justify-between items-center text-slate-200 pl-2">
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5 truncate pr-2">
<CategoryIcon icon={cat.icon} className="w-3.5 h-3.5 text-primary shrink-0" /> <CategoryIcon icon={cat.icon} className="w-3.5 h-3.5 text-primary shrink-0" />
{prod.name} <span className="truncate">{prod.name}</span>
{isFree && <span className="text-[9px] bg-green-500/20 text-green-400 px-1.5 py-0.5 rounded border border-green-500/30 font-bold uppercase">Inklusive</span>} {isFree && (
<span className="text-[9px] bg-green-500/20 text-green-400 px-1.5 py-0.2 rounded border border-green-500/30 font-bold uppercase shrink-0">
Inklusive
</span> </span>
<span className="font-bold text-xs text-white"> )}
{isFree ? '0,00 €' : new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(displayPrice)} </span>
<span className="font-bold text-xs text-white tabular-nums shrink-0">
{isFree ? '0,00 €' : fmt(displayPrice)}
</span> </span>
</div> </div>
) )
})} })}
{/* Selected Modules */} {/* Selected Module Chips */}
{sel?.moduleIds && sel.moduleIds.length > 0 && ( {sel?.moduleIds && sel.moduleIds.length > 0 && (
<div className="pl-4 space-y-1.5 pt-1"> <div className="pl-6 space-y-1 pt-0.5">
{sel.moduleIds.map((mId: string) => { {sel.moduleIds.map((mId: string) => {
let modObj: any = null let modObj: any = null
products.forEach(p => { products.forEach((p) => {
const found = p.modules?.find(m => m.id === mId) const found = p.modules?.find((m) => m.id === mId)
if (found) modObj = found if (found) modObj = found
}) })
if (!modObj) return null if (!modObj) return null
@@ -294,10 +339,12 @@ export function StepSummary({
const totalPrice = unitPrice * qty const totalPrice = unitPrice * qty
return ( return (
<div key={mId} className="flex justify-between text-slate-400 text-xs"> <div key={mId} className="flex justify-between text-slate-400 text-[11px]">
<span>+ {modObj.name} {qty > 1 ? `(${qty}x)` : ''}</span> <span className="truncate pr-2">
<span className="font-semibold text-slate-300"> + {modObj.name} {qty > 1 ? `(${qty}x)` : ''}
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(totalPrice)} </span>
<span className="font-semibold text-slate-300 tabular-nums shrink-0">
{fmt(totalPrice)}
</span> </span>
</div> </div>
) )
@@ -308,62 +355,160 @@ export function StepSummary({
) )
})} })}
</div> </div>
))} </div>
))
)}
{/* Gebühren & Zusatz-Positionen */} {/* Service- & Zusatz-Positionen */}
{linkedFeeProducts.length > 0 && ( {linkedFeeProducts.length > 0 && (
<div className="border-t border-white/5 pt-4 space-y-2"> <div className="p-3.5 rounded-2xl bg-white/5 border border-white/10 space-y-2">
<span className="text-slate-500 text-[10px] font-bold uppercase tracking-wider block">Service & Gebühren:</span> <span className="text-slate-400 text-[10px] font-bold uppercase tracking-wider block">
{linkedFeeProducts.map(fp => ( Service & Gebühren:
</span>
{linkedFeeProducts.map((fp) => (
<div key={fp.id} className="flex justify-between text-xs text-slate-300"> <div key={fp.id} className="flex justify-between text-xs text-slate-300">
<span>{fp.name}</span> <span>{fp.name}</span>
<span className="font-bold text-white">{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(fp.base_price)}</span> <span className="font-bold text-white tabular-nums">{fmt(fp.base_price)}</span>
</div> </div>
))} ))}
</div> </div>
)} )}
</div> </div>
</div>
{/* Summen-Berechnung */} {/* ─── RECHTES RASTER (lg:col-span-5) Notizen, Bedingungen & Summen ─── */}
{oneTimeTotal > 0 ? ( <div className="lg:col-span-5 space-y-5">
<div className="space-y-2 border-t border-white/10 pt-5"> {/* Glassmorphism Preiskalkulation */}
<div className="flex justify-between text-xs text-slate-400"> <div className="p-5 rounded-2xl bg-slate-950/80 border border-white/10 space-y-3 shadow-xl">
<span>Netto-Gesamtbetrag:</span> <div className="flex items-center gap-2 pb-2 border-b border-white/10">
<span className="font-semibold text-white">{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)}</span> <CreditCard className="w-4 h-4 text-primary" />
<h3 className="text-xs font-bold text-white uppercase tracking-wider">
Gesamte Preiskalkulation
</h3>
</div> </div>
<div className="flex justify-between text-xs text-slate-400">
{oneTimeTotal > 0 && (
<div className="space-y-1.5 text-xs">
<div className="flex justify-between text-slate-400">
<span>Einmalig (netto):</span>
<span className="tabular-nums font-mono text-white">{fmt(oneTimeNet)}</span>
</div>
<div className="flex justify-between text-slate-400">
<span>zzgl. 19% MwSt.:</span> <span>zzgl. 19% MwSt.:</span>
<span className="font-semibold text-white">{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)}</span> <span className="tabular-nums font-mono text-white">{fmt(oneTimeTax)}</span>
</div> </div>
{updatePriceModifier.label && ( {updatePriceModifier?.label && (
<div className="text-xs text-green-400 font-semibold bg-green-500/10 p-2.5 rounded-xl border border-green-500/20 my-1"> <div className="text-[11px] text-green-400 font-semibold bg-green-500/10 p-2 rounded-lg border border-green-500/20 my-1">
{updatePriceModifier.label} {updatePriceModifier.label}
</div> </div>
)} )}
<div className="flex justify-between text-base font-bold text-primary pt-3 border-t border-white/5"> <div className="flex justify-between text-sm font-bold text-white pt-2 border-t border-white/5">
<span>Gesamtbetrag (brutto):</span> <span>Einmalig gesamt (brutto):</span>
<span className="font-bold text-white">{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)}</span> <span className="tabular-nums font-mono text-primary font-extrabold text-base">{fmt(oneTimeGross)}</span>
</div>
</div>
) : (
<div className="space-y-2 border-t border-white/10 pt-5">
<div className="flex justify-between text-xs text-slate-400">
<span>Netto-Gesamtbetrag:</span>
<span className="font-semibold text-white">{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl.</span>
</div>
<div className="flex justify-between text-xs text-slate-400">
<span>zzgl. 19% MwSt.:</span>
<span className="font-semibold text-white">{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl.</span>
</div>
<div className="flex justify-between text-base font-bold text-primary pt-3 border-t border-white/5">
<span>Gesamtbetrag (brutto):</span>
<span className="font-bold text-white">{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl.</span>
</div> </div>
</div> </div>
)} )}
</CardContent>
{monthlyTotal > 0 && (
<div className={`space-y-1.5 text-xs ${oneTimeTotal > 0 ? 'pt-3 border-t border-white/10' : ''}`}>
<div className="flex justify-between text-slate-400">
<span>Monatlich (netto):</span>
<span className="tabular-nums font-mono text-white">{fmt(monthlyNet)} / mtl.</span>
</div>
<div className="flex justify-between text-slate-400">
<span>zzgl. 19% MwSt.:</span>
<span className="tabular-nums font-mono text-white">{fmt(monthlyTax)} / mtl.</span>
</div>
<div className="flex justify-between text-sm font-bold text-white pt-2 border-t border-white/5">
<span>Monatlich gesamt (brutto):</span>
<span className="tabular-nums font-mono text-primary font-extrabold text-base">{fmt(monthlyGross)} / mtl.</span>
</div>
</div>
)}
</div>
{/* Bestellnotizen */}
<div className="p-4 rounded-2xl bg-white/5 border border-white/10 space-y-2">
<Label htmlFor="order-notes" className="text-xs font-bold text-white uppercase tracking-wider flex items-center gap-1.5">
<MessageSquare className="w-3.5 h-3.5 text-primary" />
<span>Bestellnotizen / Bemerkungen (optional)</span>
</Label>
<textarea
id="order-notes"
rows={3}
value={orderNotes}
onChange={(e) => setOrderNotes(e.target.value)}
placeholder="Besondere Hinweise, Wunschtermine oder Ansprechpartner..."
className="w-full p-3 rounded-xl bg-slate-950/70 border border-white/10 text-xs text-white placeholder:text-slate-500 focus:border-primary focus:outline-none transition-colors resize-none scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent"
/>
</div>
{/* Lizenzbestimmungen & B2B-Workflow Checkbox */}
<div className="p-4 rounded-2xl bg-white/5 border border-white/10 space-y-3">
<div className="flex items-start gap-3">
<Checkbox
id="terms-checkbox"
checked={acceptedTerms}
onCheckedChange={(checked) => setAcceptedTerms(checked === true)}
className="rounded border-white/20 data-[state=checked]:bg-primary mt-0.5"
/>
<Label htmlFor="terms-checkbox" className="text-xs text-slate-300 leading-relaxed cursor-pointer select-none">
Ich bestätige die Richtigkeit der Angaben und akzeptiere die Lizenzvereinbarungen sowie die{' '}
<a href="/datenschutz" target="_blank" rel="noopener noreferrer" className="underline hover:text-white text-primary">
Datenschutzerklärung
</a>.
</Label>
</div>
<div className="p-2.5 rounded-xl bg-primary/10 border border-primary/20 text-[11px] text-slate-300 flex items-center gap-2">
<FileText className="w-4 h-4 text-primary shrink-0" />
<span>B2B-Freigabe: Nach Absenden wird die Lizenzierungsanfrage geprüft und freigegeben.</span>
</div>
</div>
</div>
</div>
</div>
{/* ─── 3. FIXED BOTTOM ACTION BAR (shrink-0) ─── */}
<div className="shrink-0 border-t border-white/10 bg-slate-950/90 backdrop-blur-md px-6 py-3.5 flex flex-col sm:flex-row items-center justify-between gap-3 z-10">
{/* Back Button */}
<Button
variant="outline"
onClick={prevStep}
className="w-full sm:w-auto border-white/10 text-white hover:bg-white/10 h-9 text-xs gap-1.5"
>
<ChevronLeft className="w-4 h-4" /> Zurück zu Schritt 3
</Button>
{/* Info Chip */}
<div className="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-white/5 border border-white/10 text-white text-xs">
<span className="text-slate-400">Positionen:</span>
<strong className="text-white">{finalItemsToShow.length} Kassen</strong>
<span className="text-slate-600">|</span>
<span className="text-primary font-bold">
{oneTimeTotal > 0 ? fmt(oneTimeGross) : `${fmt(monthlyGross)} / mtl.`}
</span>
</div>
{/* Submit Button */}
<Button
onClick={handleSubmit}
disabled={isSubmitting || !acceptedTerms || finalItemsToShow.length === 0}
className="w-full sm:w-auto bg-primary hover:bg-primary/90 text-white font-bold px-6 shadow-lg shadow-primary/20 h-9 text-xs gap-2"
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
{initialOrder ? 'Anfrage wird aktualisiert...' : 'Anfrage wird übertragen...'}
</>
) : (
<>
<Send className="w-3.5 h-3.5" />
{initialOrder ? 'Anfrage aktualisieren' : 'Anfrage verbindlich absenden'}
</>
)}
</Button>
</div>
</Card> </Card>
</div>
</div>
) )
} }

View File

@@ -2,20 +2,20 @@
import React from 'react' import React from 'react'
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/card' import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Pencil, Trash2, UserPlus, ChevronRight, AlertCircle, Check } from 'lucide-react' import { Badge } from '@/components/ui/badge'
import { Category, Product, CategorySelection } from '@/lib/types'
import { import {
Dialog, ChevronRight,
DialogContent, ChevronLeft,
DialogDescription, AlertCircle,
DialogFooter, Check,
DialogHeader, ShoppingCart,
DialogTitle, Save,
} from '@/components/ui/dialog' Plus,
} from 'lucide-react'
import { Category, Product, CategorySelection } from '@/lib/types'
interface SummarySidebarProps { interface SummarySidebarProps {
visibleCategories: Category[] visibleCategories: Category[]
@@ -34,19 +34,15 @@ interface SummarySidebarProps {
updatePriceModifier: { label?: string } updatePriceModifier: { label?: string }
allCategoriesFilled: boolean allCategoriesFilled: boolean
productValidationErrors: string[] productValidationErrors: string[]
basketItems: any[]
editingIdx: number | null editingIdx: number | null
editBasketItem: (idx: number) => void
deleteBasketItem: (idx: number) => void
deviceName: string deviceName: string
setDeviceName: (name: string) => void setDeviceName: (name: string) => void
licenseNumber: string licenseNumber: string
setLicenseNumber: (num: string) => void setLicenseNumber: (num: string) => void
addToBasket: () => void onSaveAndProceed: () => void
isNextStepDisabled: boolean isNextStepDisabled: boolean
hasActiveSelection: boolean hasActiveSelection: boolean
showValidationWarning?: boolean showValidationWarning?: boolean
nextStep: () => void
prevStep: () => void prevStep: () => void
} }
@@ -67,40 +63,56 @@ export function SummarySidebar({
updatePriceModifier, updatePriceModifier,
allCategoriesFilled, allCategoriesFilled,
productValidationErrors, productValidationErrors,
basketItems,
editingIdx, editingIdx,
editBasketItem,
deleteBasketItem,
deviceName, deviceName,
setDeviceName, setDeviceName,
licenseNumber, licenseNumber,
setLicenseNumber, setLicenseNumber,
addToBasket, onSaveAndProceed,
isNextStepDisabled, isNextStepDisabled,
hasActiveSelection,
showValidationWarning = false,
nextStep,
prevStep, prevStep,
}: SummarySidebarProps) { }: SummarySidebarProps) {
const fmt = (val: number) => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val) const fmt = (val: number) => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(val)
return ( const isEditing = editingIdx !== null
<div className="h-full flex flex-col"> const currentDeviceTitle = isEditing
<Card className="bg-slate-900/80 backdrop-blur-xl border border-white/10 shadow-2xl rounded-2xl overflow-hidden flex flex-col h-full"> ? `Kasse ${editingIdx + 1}`
<CardHeader className="shrink-0 pb-3 border-b border-white/5"> : (deviceName.trim() || 'Neue Kasse')
<CardTitle className="text-white text-base font-bold flex items-center justify-between">
<span>Zusammenfassung</span>
{basketItems.length > 0 && (
<span className="text-xs bg-sky-500/20 text-sky-300 border border-sky-500/30 px-2 py-0.5 rounded-full font-semibold">
{basketItems.length} {basketItems.length === 1 ? 'Kasse' : 'Kassen'}
</span>
)}
</CardTitle>
</CardHeader>
<CardContent className="p-4 pt-3 flex-1 flex flex-col overflow-hidden gap-3">
{/* Active Selections & Price - scrollable */} const isSaveDisabled = !allCategoriesFilled || productValidationErrors.length > 0
<div className="space-y-3 flex-1 overflow-y-auto pr-1 scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
return (
<Card className="glass-dark border-white/10 shadow-2xl rounded-2xl overflow-hidden flex flex-col h-full bg-slate-900/90 backdrop-blur-xl">
{/* ─── 1. TOP HEADER (shrink-0) ─── */}
<CardHeader className="shrink-0 pb-3 pt-4 px-4 border-b border-white/10 bg-slate-950/60">
<div className="flex items-center justify-between gap-2">
<CardTitle className="text-white text-sm font-bold flex items-center gap-2">
<ShoppingCart className="w-4 h-4 text-primary" />
<span>Kassen-Konfiguration</span>
</CardTitle>
<Badge
variant="outline"
className={`text-[10px] px-2 py-0.5 font-semibold ${
isEditing
? 'border-amber-500/40 bg-amber-500/10 text-amber-400'
: 'border-primary/40 bg-primary/10 text-primary'
}`}
>
{isEditing ? `Bearbeite #${editingIdx + 1}` : 'Neuer Eintrag'}
</Badge>
</div>
</CardHeader>
{/* ─── 2. SCROLLABLE MIDDLE (flex-1 min-h-0 overflow-y-auto) ─── */}
<CardContent className="p-4 flex-1 min-h-0 overflow-y-auto space-y-4 scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent">
{/* Aktuelle Kassen-Auswahl */}
<div className="space-y-3">
<div className="flex items-center justify-between text-xs font-bold text-slate-300 uppercase tracking-wider">
<span>Gewählte Positionen</span>
</div>
{/* Selected Categories List */}
<div className="space-y-2">
{visibleCategories.map(cat => { {visibleCategories.map(cat => {
const sel = selections[cat.id] const sel = selections[cat.id]
const selectedProds: Product[] = [] const selectedProds: Product[] = []
@@ -115,13 +127,13 @@ export function SummarySidebar({
} }
if (selectedProds.length === 0) return ( if (selectedProds.length === 0) return (
<div key={cat.id} className="text-xs text-slate-500 italic flex items-center gap-1.5"> <div key={cat.id} className="text-[11px] text-slate-500 italic flex items-center gap-1.5">
{cat.is_required ? ( {cat.is_required ? (
<AlertCircle className="w-3 h-3 text-destructive shrink-0" /> <AlertCircle className="w-3 h-3 text-destructive shrink-0" />
) : ( ) : (
<span className="w-3 h-3 inline-block shrink-0" /> <span className="w-3 h-3 inline-block shrink-0" />
)} )}
<span>{cat.name}: {cat.is_required ? 'nicht gewählt' : 'nicht gewählt (optional)'}</span> <span>{cat.name}: {cat.is_required ? 'nicht gewählt (Pflicht)' : 'nicht gewählt (optional)'}</span>
</div> </div>
) )
@@ -129,27 +141,26 @@ export function SummarySidebar({
const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0 const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0
return ( return (
<div key={cat.id} className="space-y-1"> <div key={cat.id} className="space-y-0.5 text-xs">
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wider">{cat.name}</p> <p className="text-[10px] font-semibold text-slate-400 uppercase tracking-wider">{cat.name}</p>
{sortedProds.map((prod, idx) => { {sortedProds.map((prod, idx) => {
const isFree = idx < freeLimit const isFree = idx < freeLimit
const actualPrice = isFree ? 0 : prod.base_price const actualPrice = isFree ? 0 : prod.base_price
return ( return (
<div key={prod.id} className="space-y-0.5 pl-2"> <div key={prod.id} className="pl-1.5 space-y-0.5">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-white text-xs">
<span className="text-white">{prod.name} {isFree && <span className="text-[10px] text-green-400">(Frei)</span>}</span> <span className="truncate pr-2">{prod.name} {isFree && <span className="text-[10px] text-green-400">(Inkl.)</span>}</span>
<span className="text-white tabular-nums"> <span className="tabular-nums shrink-0 font-medium">
{fmt(actualPrice)} {fmt(actualPrice)}
{' '}<span className="text-slate-400 text-xs">{billingLabel(prod.billing_interval)}</span>
</span> </span>
</div> </div>
{sel.moduleIds.map(mId => { {sel.moduleIds.map(mId => {
const mod = prod.modules?.find(m => m.id === mId) const mod = prod.modules?.find(m => m.id === mId)
const qty = moduleQuantities[mId] || 1 const qty = moduleQuantities[mId] || 1
return mod ? ( return mod ? (
<div key={mId} className="flex justify-between text-xs pl-2"> <div key={mId} className="flex justify-between text-[11px] text-slate-400 pl-2">
<span className="text-slate-300">+ {mod.name} {mod.has_quantity ? `(x${qty})` : ''}</span> <span className="truncate pr-2">+ {mod.name} {mod.has_quantity ? `(x${qty})` : ''}</span>
<span className="text-slate-300 tabular-nums"> <span className="tabular-nums shrink-0 font-medium text-slate-300">
{fmt(mod.price * qty)} {fmt(mod.price * qty)}
</span> </span>
</div> </div>
@@ -161,150 +172,93 @@ export function SummarySidebar({
</div> </div>
) )
})} })}
</div>
{/* Pricing block */} {/* Pricing Box */}
<Separator className="bg-white/10" /> <div className="p-3 rounded-xl bg-slate-950/80 border border-white/10 space-y-2 text-xs">
{oneTimeTotal > 0 && monthlyTotal > 0 ? ( {oneTimeTotal > 0 && (
<div className="space-y-3 bg-slate-900/90 border border-white/10 rounded-xl p-3 shadow-inner">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Einmalig:</p> <div className="flex justify-between text-slate-400">
<div className="flex justify-between text-xs text-slate-400"> <span>Kaufpreis Kasse (netto):</span>
<span>Netto:</span> <span className="tabular-nums font-mono">{fmt(oneTimeNet)}</span>
<span className="tabular-nums">{fmt(oneTimeNet)}</span>
</div> </div>
<div className="flex justify-between text-xs text-slate-400"> <div className="flex justify-between font-bold text-white">
<span>zzgl. 19% MwSt.:</span> <span>Kaufpreis Kasse (brutto):</span>
<span className="tabular-nums">{fmt(oneTimeTax)}</span> <span className="tabular-nums font-mono text-primary">{fmt(oneTimeGross)}</span>
</div>
<div className="flex justify-between text-sm font-extrabold text-white pt-1">
<span>Brutto:</span>
<span className="tabular-nums text-sky-400">{fmt(oneTimeGross)}</span>
</div>
</div>
<div className="space-y-1 pt-2 border-t border-white/10">
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Monatlich:</p>
<div className="flex justify-between text-xs text-slate-400">
<span>Netto:</span>
<span className="tabular-nums">{fmt(monthlyNet)} / mtl.</span>
</div>
<div className="flex justify-between text-xs text-slate-400">
<span>zzgl. 19% MwSt.:</span>
<span className="tabular-nums">{fmt(monthlyTax)} / mtl.</span>
</div>
<div className="flex justify-between text-sm font-extrabold text-white pt-1">
<span>Brutto:</span>
<span className="tabular-nums text-sky-400">{fmt(monthlyGross)} / mtl.</span>
</div>
</div>
</div>
) : oneTimeTotal > 0 ? (
<div className="space-y-1 bg-slate-900/90 border border-white/10 rounded-xl p-3 shadow-inner">
<div className="flex justify-between text-xs text-slate-400">
<span>Netto:</span>
<span className="tabular-nums">{fmt(oneTimeNet)}</span>
</div>
<div className="flex justify-between text-xs text-slate-400">
<span>zzgl. 19% MwSt.:</span>
<span className="tabular-nums">{fmt(oneTimeTax)}</span>
</div>
<div className="flex justify-between text-base font-extrabold text-sky-400 pt-1">
<span>Gesamt (brutto):</span>
<span className="tabular-nums">{fmt(oneTimeGross)}</span>
</div>
</div>
) : (
<div className="space-y-1 bg-slate-900/90 border border-white/10 rounded-xl p-3 shadow-inner">
<div className="flex justify-between text-xs text-slate-400">
<span>Netto:</span>
<span className="tabular-nums">{fmt(monthlyNet)} / mtl.</span>
</div>
<div className="flex justify-between text-xs text-slate-400">
<span>zzgl. 19% MwSt.:</span>
<span className="tabular-nums">{fmt(monthlyTax)} / mtl.</span>
</div>
<div className="flex justify-between text-base font-extrabold text-sky-400 pt-1">
<span>Gesamt (brutto):</span>
<span className="tabular-nums">{fmt(monthlyGross)} / mtl.</span>
</div> </div>
</div> </div>
)} )}
{updatePriceModifier.label && ( {monthlyTotal > 0 && (
<div className="text-xs text-green-400 bg-green-500/10 p-2.5 rounded-lg border border-green-500/20 space-y-1"> <div className={`space-y-1 ${oneTimeTotal > 0 ? 'pt-2 border-t border-white/10' : ''}`}>
<p className="font-semibold flex items-center gap-1"> <div className="flex justify-between text-slate-400">
<Check className="w-3.5 h-3.5" /> <span>Mietpreis Kasse (netto):</span>
Update-Rabatt aktiv <span className="tabular-nums font-mono">{fmt(monthlyNet)} / mtl.</span>
</p>
<p>{updatePriceModifier.label}</p>
</div> </div>
)} <div className="flex justify-between font-bold text-white">
{!allCategoriesFilled && showValidationWarning && ( <span>Mietpreis Kasse (brutto):</span>
<div className="text-xs text-red-400 flex items-start gap-2 bg-red-500/10 p-3 rounded-xl border border-red-500/40 animate-eye-catcher"> <span className="tabular-nums font-mono text-primary">{fmt(monthlyGross)} / mtl.</span>
<AlertCircle className="w-4 h-4 shrink-0 text-red-400 mt-0.5" />
<div>
<p className="font-extrabold uppercase tracking-wider text-[11px] text-red-300">Pflichtkategorie fehlt!</p>
<p className="text-[11px] text-slate-300 leading-relaxed">Bitte wählen Sie aus allen rot markierten Pflichtkategorien einen Artikel aus.</p>
</div> </div>
</div> </div>
)} )}
{!allCategoriesFilled && !showValidationWarning && (
<div className="text-xs text-slate-400 flex items-start gap-2 bg-slate-900/50 p-2.5 rounded-xl border border-white/5">
<AlertCircle className="w-4 h-4 shrink-0 text-slate-500 mt-0.5" />
<div>
<p className="font-semibold text-[11px] text-slate-300">Pflichtkategorien beachten</p>
<p className="text-[11px] text-slate-400">Bitte wählen Sie für die Rot markierten Kategorien ein Produkt aus.</p>
</div> </div>
</div> </div>
)}
{productValidationErrors.map((err, errIdx) => ( {/* Gerätename & Lizenznummer Eingabe */}
<p key={errIdx} className="text-xs text-red-400 flex items-center gap-1 bg-red-500/10 p-2 rounded-lg border border-red-500/20"> <div className="pt-2 border-t border-white/10 space-y-2.5">
<AlertCircle className="w-3.5 h-3.5 shrink-0 text-red-400" />
{err}
</p>
))}
</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"> <p className="text-xs font-bold text-white uppercase tracking-wider">
{editingIdx !== null ? `Kasse ${editingIdx + 1} bearbeiten` : `Kassenbezeichnung`} {isEditing ? `Kasse anpassen` : `Kassenbezeichnung`}
</p> </p>
<div className="space-y-1.5"> <div className="space-y-1">
<Label htmlFor="device-name-sidebar" className="text-[11px] text-slate-400">Kassenname (optional)</Label> <Label htmlFor="device-name-sidebar" className="text-[11px] text-slate-400">Kassenname (optional)</Label>
<Input <Input
id="device-name-sidebar" id="device-name-sidebar"
placeholder={`z.B. Kasse ${basketItems.length + 1}, Theke`} placeholder="z. B. Kasse 1, Hauptkasse, Theke"
value={deviceName} value={deviceName}
onChange={e => setDeviceName(e.target.value)} onChange={e => setDeviceName(e.target.value)}
className="bg-white/5 border-white/10 text-white text-xs h-8 rounded-lg" className="bg-white/5 border-white/10 text-white text-xs h-8 rounded-lg"
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1">
<Label htmlFor="license-number-sidebar" className="text-[11px] text-slate-400">Lizenznummer (optional)</Label> <Label htmlFor="license-number-sidebar" className="text-[11px] text-slate-400">Lizenznummer (optional)</Label>
<Input <Input
id="license-number-sidebar" id="license-number-sidebar"
placeholder="z.B. LIZ-12345 (eindeutig)" placeholder="z. B. LIZ-12345"
value={licenseNumber} value={licenseNumber}
onChange={e => setLicenseNumber(e.target.value)} onChange={e => setLicenseNumber(e.target.value)}
className="bg-white/5 border-white/10 text-white text-xs h-8 rounded-lg" className="bg-white/5 border-white/10 text-white text-xs h-8 rounded-lg font-mono uppercase"
/> />
</div> </div>
</div> </div>
</CardContent> </CardContent>
<CardFooter className="flex flex-col gap-2 shrink-0 p-4 pt-2 border-t border-white/10 bg-slate-950/80">
{/* ─── 3. FIXED BOTTOM ACTIONS (shrink-0) ─── */}
<CardFooter className="flex flex-col gap-2 shrink-0 p-4 border-t border-white/10 bg-slate-950/90">
{/* Primary Action Button: Save & Proceed immediately to Step 4 */}
<Button <Button
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" className="w-full h-11 text-xs font-bold bg-primary hover:bg-primary/90 text-white shadow-lg shadow-primary/20 gap-2"
onClick={nextStep} onClick={onSaveAndProceed}
disabled={isNextStepDisabled && basketItems.length === 0} disabled={isSaveDisabled}
> >
{editingIdx !== null ? ( {isEditing ? (
<>Änderungen für &quot;{basketItems[editingIdx]?.deviceName || `Kasse ${editingIdx + 1}`}&quot; übernehmen</> <>
<Save className="w-4 h-4" /> Änderungen speichern & zur Übersicht <ChevronRight className="w-4 h-4 ml-0.5" />
</>
) : ( ) : (
<>Weiter zu Schritt 4 <ChevronRight className="ml-1 w-4 h-4" /></> <>
<Plus className="w-4 h-4" /> Kasse zum Auftrag hinzufügen <ChevronRight className="w-4 h-4 ml-0.5" />
</>
)} )}
</Button> </Button>
<Button variant="ghost" className="w-full text-slate-400 hover:text-white h-7 text-xs" onClick={prevStep}>
Zurück zum Abrechnungsmodell {/* Back Button */}
<Button
variant="ghost"
className="w-full text-slate-400 hover:text-white h-7 text-xs"
onClick={prevStep}
>
<ChevronLeft className="w-3 h-3 mr-1" /> Zurück zum Abrechnungsmodell
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
</div>
) )
} }