From 694e8dfbc1c6c5a11de0c8674d5914f2d8b605e1 Mon Sep 17 00:00:00 2001 From: DanielS Date: Mon, 7 Sep 2026 22:26:13 +0200 Subject: [PATCH] refactor(wizard): optimize ui ux with no-page-scroll and hub flow --- shop/components/order-wizard.tsx | 19 +- shop/components/wizard/step-billing.tsx | 277 +++-- shop/components/wizard/step-customer.tsx | 1097 ++++++++++++-------- shop/components/wizard/step-software.tsx | 165 +-- shop/components/wizard/step-summary.tsx | 661 +++++++----- shop/components/wizard/summary-sidebar.tsx | 326 +++--- 6 files changed, 1523 insertions(+), 1022 deletions(-) diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index ff5f1d8..c4552f4 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -202,7 +202,7 @@ export function OrderWizard({ // Formular-State für neuen Endkunden 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: '', }) const [isCreatingCustomer, setIsCreatingCustomer] = useState(false) @@ -614,7 +614,7 @@ export function OrderWizard({ setSelectedEndCustomerId(created.id) setCustomerMode('select') 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: '', }) } catch (e: any) { @@ -624,7 +624,7 @@ export function OrderWizard({ } } - const addToBasket = async () => { + const addToBasket = async (proceedToSummary: boolean = false) => { const normLicense = licenseNumber.trim().toUpperCase() if (normLicense) { const isDuplicateInBasket = basketItems.some( @@ -676,6 +676,11 @@ export function OrderWizard({ } }) setSelections(resetSels) + + if (proceedToSummary) { + setDirection(1) + setStep(4) + } } const resetFormForNewDevice = () => { @@ -1098,6 +1103,8 @@ export function OrderWizard({ existingModuleIds={existingModuleIds} activeCategoryId={activeCategoryId} editingDeviceName={editingIdx !== null ? (basketItems[editingIdx]?.deviceName || `Kasse ${editingIdx + 1}`) : null} + onCancelEdit={resetFormForNewDevice} + productValidationErrors={productValidationErrors} /> @@ -1122,19 +1129,15 @@ export function OrderWizard({ updatePriceModifier={updatePriceModifier} allCategoriesFilled={allCategoriesFilled} productValidationErrors={productValidationErrors} - basketItems={basketItems} editingIdx={editingIdx} - editBasketItem={editBasketItem} - deleteBasketItem={deleteBasketItem} deviceName={deviceName} setDeviceName={setDeviceName} licenseNumber={licenseNumber} setLicenseNumber={setLicenseNumber} - addToBasket={addToBasket} + onSaveAndProceed={() => addToBasket(true)} isNextStepDisabled={isNextStepDisabled} hasActiveSelection={hasActiveSelection} showValidationWarning={showValidationWarning} - nextStep={nextStep} prevStep={prevStep} /> diff --git a/shop/components/wizard/step-billing.tsx b/shop/components/wizard/step-billing.tsx index 75d10e6..fa4bf42 100644 --- a/shop/components/wizard/step-billing.tsx +++ b/shop/components/wizard/step-billing.tsx @@ -1,11 +1,24 @@ 'use client' 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 { Button } from '@/components/ui/button' 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 { selectedBillingInterval: 'one_time' | 'monthly' @@ -28,105 +41,207 @@ export function StepBilling({ prevStep, nextStep, }: 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 ( - - - - - Abrechnungsmodell wählen - - - Möchten Sie Lizenzen dauerhaft kaufen oder monatlich mieten? - + + {/* ─── 1. FIXED HEADER (shrink-0) ─── */} + +
+
+ + + Abrechnungsmodell wählen + + + Wählen Sie zwischen monatlicher Miete (Abonnement) oder einmaligem Softwarekauf. + +
+ + Schritt 2 von 4 + +
- -
- {/* Option 1: Kauf */} -
handleBillingIntervalChange('one_time')} - 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' - ? 'border-primary bg-primary/10 shadow-[0_0_25px_rgba(59,130,246,0.2)]' - : 'border-white/5 bg-white/5 hover:border-white/20' + + {/* ─── 2. SCROLLABLE MIDDLE CONTENT (flex-1 min-h-0 overflow-y-auto) ─── */} +
+
+ {/* Abrechnungsmodell Cards */} +
+ {/* Option 1: Miete / Abo */} +
handleBillingIntervalChange('monthly')} + className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between min-h-[14rem] select-none ${ + isMonthly + ? '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]' }`} - > -
+ >
-
-
- +
+
+
-

Einmaliger Kauf

+ {isMonthly && ( +
+ +
+ )}
-

- Einmalige Anschaffungskosten für die Softwarelizenz. Keine monatlichen Mietgebühren. + +

+
+

Miete / Abo

+ + Empfohlen + +
+

+ Fortlaufende monatliche Abrechnung +

+
+ +

+ Maximale Flexibilität: Inklusive sämtlicher Software-Updates, Cloud-Dienste und technischem Support. Jederzeit kündbar oder anpassbar.

- {selectedBillingInterval === 'one_time' && ( -
- -
- )} +
+ Zahlungsintervall: + monatlich +
+
- {selectedBillingInterval === 'one_time' && customerMode === 'select' && selectedEndCustomerId && ( -
e.stopPropagation()} - className="mt-4 pt-4 border-t border-white/5 space-y-2 cursor-default" - > - - setLastLicenseDate(e.target.value)} - className="bg-[#0b1329] border-white/10 text-white text-xs h-8 focus:border-primary" - /> -

- Ermöglicht automatische Ermittlung von Update-Gebühren. + {/* Option 2: Kauf / Einmalkauf */} +

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]' + }`} + > +
+
+
+ +
+ {isOneTime && ( +
+ +
+ )} +
+ +
+
+

Kauf / Einmalkauf

+ + Klassisch + +
+

+ Einmalige Lizenzgebühr

- )} + +

+ Dauerhafte Lizenzrechte für die gekaufte Version inklusive 12 Monate kostenfreier Updates. Keine laufenden monatlichen Mietgebühren. +

+
+ +
+ Zahlungsintervall: + einmalig +
- {/* Option 2: Abo */} -
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 === 'monthly' - ? 'border-primary bg-primary/10 shadow-[0_0_25px_rgba(59,130,246,0.2)]' - : 'border-white/5 bg-white/5 hover:border-white/20' - }`} - > -
-
-
- + {/* Dynamische Stichtagsabfrage bei Kauf */} + + {isOneTime && ( + +
+
+
+ +
+
+ +

+ Datum des letzten Kaufs zur automatischen Berechnung der Update-Staffel (z. B. 15% innerhalb 1 Jahr, 30% bis 2 Jahre, 50% bis 3 Jahre). +

+
+
+ +
+
+ + setLastLicenseDate(e.target.value)} + className="pl-9 bg-slate-950/80 border-white/10 text-white text-xs h-9 focus:border-primary" + /> +
+ {lastLicenseDate && ( + + Stichtag gesetzt + + )} +
-

Monatliches Abo (Miete)

-
-

- Laufende monatliche Gebühren. Inklusive aller Updates und flexibler Laufzeit. -

-
- {selectedBillingInterval === 'monthly' && ( -
- -
+ )} -
+
- - -
+ + {/* ─── 3. FIXED BOTTOM ACTION BAR (shrink-0) ─── */} +
+ {/* Back Button */} + - - +
) } diff --git a/shop/components/wizard/step-customer.tsx b/shop/components/wizard/step-customer.tsx index c43dca4..9786a1d 100644 --- a/shop/components/wizard/step-customer.tsx +++ b/shop/components/wizard/step-customer.tsx @@ -2,7 +2,7 @@ import React, { useState, useMemo, useEffect } from 'react' 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 { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -19,6 +19,14 @@ import { ChevronsLeft, ChevronsRight, Users, + ChevronDown, + ChevronUp, + X, + CreditCard, + Mail, + MapPin, + CheckCircle2, + Building, } from 'lucide-react' import { EndCustomer } from '@/lib/types' @@ -65,6 +73,18 @@ export function StepCustomer({ const [partnerPageSize, setPartnerPageSize] = useState(10) const [partnerCurrentPage, setPartnerCurrentPage] = useState(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(() => { setPartnerCurrentPage(1) }, [partnerSearchTerm, partnerPageSize]) @@ -123,7 +143,8 @@ export function StepCustomer({ c.first_name?.toLowerCase().includes(term) || c.last_name?.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]) @@ -135,6 +156,11 @@ export function StepCustomer({ return filteredCustomers.slice(from, from + 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 const handleSelectCustomer = (customer: EndCustomer) => { setSelectedEndCustomerId(customer.id) @@ -143,261 +169,260 @@ 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 ( - - - - - Für wen bestellen Sie? - - - Wählen Sie den Partner (für Admins) und den Endkunden aus oder legen Sie einen neuen an. - - - - {/* Modus-Toggle: Bestandskunde vs. Neuer Kunde */} -
- - -
+ <> + + {/* ─── 1. FIXED HEADER (shrink-0) ─── */} + +
+
+ + + Für wen bestellen Sie? + + + Wählen Sie den Endkunden für die Lizenzierung aus oder legen Sie schnell einen neuen Datensatz an. + +
+ +
+
-
- {/* ─── 1. ADMIN PARTNER SELECTION ─── */} - {isAdmin && ( -
-
-
- -

- Wählen Sie einen Partner aus, um dessen Kunden anzuzeigen, oder "Alle Partner". -

-
- - Admin Modus - -
- - {/* Partner Header Controls: Search & Page Size */} -
-
- - 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" - /> + {/* ─── 2. SCROLLABLE MIDDLE CONTENT (flex-1 min-h-0 overflow-hidden) ─── */} +
+
+ {/* ─── 2A. ADMIN PARTNER SELECTION ─── */} + {isAdmin && ( +
+
+
+ +

+ Filtert die Kundenliste nach Partner +

+
+ + Admin Modus +
-
- Partner pro Seite: + {/* Partner Search & Page Size */} +
+
+ + setPartnerSearchTerm(e.target.value)} + className="pl-8 bg-white/5 border-white/10 text-white placeholder:text-slate-500 focus:border-primary text-xs h-8" + /> +
-
- {/* Partner Card List */} -
- {/* Reserved Fixed Card: Alle Partner */} -
{ - setSelectedCompanyId('all') - setSelectedEndCustomerId(null) - }} - className={`flex items-center justify-between p-3.5 rounded-xl border-2 cursor-pointer transition-all select-none ${ - selectedCompanyId === 'all' || !selectedCompanyId - ? 'border-primary bg-primary/10 shadow-lg shadow-primary/5' - : 'border-white/5 bg-white/5 hover:bg-white/10' - }`} - > -
-
- + {/* Partner Card List (Scrollable) */} +
+ {/* Reserved Fixed Card: Alle Partner */} +
{ + setSelectedCompanyId('all') + setSelectedEndCustomerId(null) + }} + className={`flex items-center justify-between p-2.5 rounded-xl border cursor-pointer transition-all select-none ${ + selectedCompanyId === 'all' || !selectedCompanyId + ? 'border-primary bg-primary/10 shadow-sm shadow-primary/10' + : 'border-white/5 bg-white/5 hover:bg-white/10' + }`} + > +
+
+ +
+
+

+ Alle Partner (Firmenübergreifend) +

+

+ {endCustomers.filter((c) => !c.is_anonymized).length} Kunden gesamt +

+
-
-

- Alle Partner (Firmenübergreifend) -

-

- Zeigt Kunden aller registrierten Partnerunternehmen an -

-
-
-
- - {endCustomers.filter((c) => !c.is_anonymized).length} Kunden gesamt - {(selectedCompanyId === 'all' || !selectedCompanyId) && ( - + )}
-
- {/* Dynamic Partner Cards */} - {paginatedCompanies.map((c: any) => { - const isSelected = selectedCompanyId === c.id - const customerCount = customerCountMap[c.id] || 0 + {/* Dynamic Partner Cards */} + {paginatedCompanies.map((c: any) => { + const isSelected = selectedCompanyId === c.id + const customerCount = customerCountMap[c.id] || 0 - return ( -
{ - setSelectedCompanyId(c.id) - setSelectedEndCustomerId(null) - }} - className={`flex items-center justify-between p-3.5 rounded-xl border-2 cursor-pointer transition-all select-none ${ - isSelected - ? 'border-primary bg-primary/10 shadow-lg shadow-primary/5' - : 'border-white/5 bg-white/5 hover:bg-white/10' - }`} - > -
-
- + return ( +
{ + setSelectedCompanyId(c.id) + setSelectedEndCustomerId(null) + }} + className={`flex items-center justify-between p-2.5 rounded-xl border cursor-pointer transition-all select-none ${ + isSelected + ? 'border-primary bg-primary/10 shadow-sm shadow-primary/10' + : 'border-white/5 bg-white/5 hover:bg-white/10' + }`} + > +
+
+ +
+
+

{c.name}

+ {(c.street || c.zip || c.city) && ( +

+ {[c.street, [c.zip, c.city].filter(Boolean).join(' ')].filter(Boolean).join(', ')} +

+ )} +
-
-

{c.name}

- {(c.street || c.zip || c.city) && ( -

- {[c.street, [c.zip, c.city].filter(Boolean).join(' ')].filter(Boolean).join(', ')} -

- )} + +
+ 0 + ? 'border-green-500/30 text-green-400 bg-green-500/10' + : 'border-white/10 text-slate-500 bg-white/5' + }`} + > + {customerCount} + + {isSelected && }
+ ) + })} -
- 0 - ? 'border-green-500/30 text-green-400 bg-green-500/10' - : 'border-white/10 text-slate-500 bg-white/5' - }`} - > - {customerCount} {customerCount === 1 ? 'Kunde' : 'Kunden'} - - {isSelected && } -
+ {filteredCompanies.length === 0 && ( +
+ Keine Partner gefunden.
- ) - })} - - {filteredCompanies.length === 0 && ( -
- Keine Partner mit dem Suchbegriff "{partnerSearchTerm}" gefunden. -
- )} -
- - {/* Partner Pagination Controls */} -
-
- Zeige {totalPartnerCount === 0 ? 0 : (partnerCurrentPage - 1) * partnerPageSize + 1} bis{' '} - {Math.min(partnerCurrentPage * partnerPageSize, totalPartnerCount)} von {totalPartnerCount} Partnern + )}
-
- - - - - Seite {partnerCurrentPage} von {totalPartnerPages} - - - - -
-
-
- )} - - - {/* ─── 2. CUSTOMER SELECTION ─── */} - {customerMode === 'select' && ( - -
+ {/* Partner Pagination Footer */} +
- -

- {isAdmin && selectedCompanyId && selectedCompanyId !== 'all' - ? `Endkunden für den Partner "${companies.find((c) => c.id === selectedCompanyId)?.name || selectedCompanyId}"` - : 'Alle verfügbaren Endkunden'} -

+ {Math.min(partnerCurrentPage * partnerPageSize, totalPartnerCount)} / {totalPartnerCount} Partner +
+
+ + + + {partnerCurrentPage} / {totalPartnerPages} + + +
+
+ )} + {/* ─── 2B. CUSTOMER SELECTION LIST (Scrollable Panel) ─── */} +
+
+
+ +

+ {isAdmin && selectedCompanyId && selectedCompanyId !== 'all' + ? `Partner: "${companies.find((c) => c.id === selectedCompanyId)?.name || selectedCompanyId}"` + : 'Alle Endkunden'} +

+
+ +
+ + {/* Customer Search */} +
+ + setSearchTerm(e.target.value)} + className="pl-8 bg-white/5 border-white/10 text-white placeholder:text-slate-500 text-xs h-8" + /> +
+ + {/* Customer Card List (Isolated Scrollable Area) */} +
{customersByCompany.length === 0 ? ( -
- -

+

+ +

{isAdmin && selectedCompanyId && selectedCompanyId !== 'all' ? 'Keine Endkunden für den ausgewählten Partner vorhanden.' : 'Noch keine Endkunden vorhanden.'} @@ -405,228 +430,448 @@ export function StepCustomer({

+ ) : filteredCustomers.length === 0 ? ( +
+ Keine passenden Kunden für "{searchTerm}" gefunden. +
) : ( -
- {/* Customer Header Controls: Search & Page Size */} -
-
- + paginatedCustomers.map((customer) => { + const isSelected = selectedEndCustomerId === customer.id + + return ( +
handleSelectCustomer(customer)} + className={`flex items-start p-3 rounded-xl border-2 cursor-pointer transition-all select-none ${ + isSelected + ? 'border-primary bg-primary/10 shadow-md shadow-primary/5' + : 'border-white/5 bg-white/5 hover:bg-white/10' + }`} + > + handleSelectCustomer(customer)} + className="sr-only" + /> +
+
+

{customer.company_name}

+ {isAdmin && customer.partner_id && ( + + Partner: {companies.find((co) => co.id === customer.partner_id)?.name || customer.partner_id} + + )} +
+

+ {[customer.first_name, customer.last_name].filter(Boolean).join(' ')} + {customer.first_name || customer.last_name ? ' · ' : ''} + {[customer.street, customer.zip, customer.city].filter(Boolean).join(', ')} +

+ {customer.email && ( +

+ {customer.email} +

+ )} +
+ {isSelected && } +
+ ) + }) + )} +
+ + {/* Customer Pagination */} + {totalCustomerCount > 0 && ( +
+
+ + {Math.min(currentPage * pageSize, totalCustomerCount)} / {totalCustomerCount} Kunden + + +
+ +
+ + + + {currentPage} / {totalCustomerPages} + + + +
+
+ )} +
+
+
+ + {/* ─── 3. FIXED BOTTOM ACTION BAR (shrink-0) ─── */} +
+ {/* Selected Customer Summary Chip */} +
+ {activeCustomer ? ( +
+ +
+ Ausgewählt:{' '} + {activeCustomer.company_name} + {(activeCustomer.city || activeCustomer.zip) && ( + + ({[activeCustomer.zip, activeCustomer.city].filter(Boolean).join(' ')}) + + )} +
+
+ ) : ( +
+ + Bitte wählen Sie einen Kunden aus, um fortzufahren. +
+ )} +
+ + {/* Next Button */} + +
+ + + {/* ─── 4. SLIDE-OVER DRAWER FOR CUSTOMER CREATION ─── */} + + {isDrawerOpen && ( +
+ {/* Backdrop */} + + +
+ + {/* Drawer Header */} +
+
+
+ +
+
+

Neuen Kunden anlegen

+

+ {isAdmin && selectedCompanyId && selectedCompanyId !== 'all' + ? `Für Partner: ${companies.find((c) => c.id === selectedCompanyId)?.name || selectedCompanyId}` + : 'Erstellen und direkt für die Bestellung auswählen'} +

+
+
+ +
+ + {/* Drawer Body / Form */} +
+ {/* Basic Info Section */} +
+
+ + + Stammdaten & Anschrift + +
+ +
+
+ setSearchTerm(e.target.value)} - className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-500 text-sm h-9" + 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" />
- {/* Page Size Selector */} -
- Kunden pro Seite: - +
+ + + 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" + /> +
+ +
+ + + 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" + /> +
+ +
+ + + 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" + /> +
+ +
+ + + 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" + /> +
+ +
+ + + 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" + /> +
+ +
+ + + 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" + /> +
+ +
+ + + 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" + />
+
- {filteredCustomers.length === 0 ? ( -
- Keine passenden Kunden gefunden. -
- ) : ( -
- {/* Customer Card List */} -
- {paginatedCustomers.map((customer) => { - const isSelected = selectedEndCustomerId === customer.id - - return ( -
handleSelectCustomer(customer)} - className={`flex items-start p-3.5 rounded-xl border-2 cursor-pointer transition-all select-none ${ - isSelected - ? 'border-primary bg-primary/10 shadow-lg shadow-primary/5' - : 'border-white/5 bg-white/5 hover:bg-white/10' - }`} - > - handleSelectCustomer(customer)} - className="sr-only" - /> -
-
-

{customer.company_name}

- {isAdmin && customer.partner_id && ( - - Partner: {companies.find((co) => co.id === customer.partner_id)?.name || customer.partner_id} - - )} -
-

- {[customer.first_name, customer.last_name].filter(Boolean).join(' ')} - {customer.first_name || customer.last_name ? ' · ' : ''} - {[customer.street, customer.zip, customer.city].filter(Boolean).join(', ')} -

-
- {isSelected && } -
- ); - })} -
- - {/* Customer Pagination Footer */} -
-
- Zeige {totalCustomerCount === 0 ? 0 : (currentPage - 1) * pageSize + 1} bis{' '} - {Math.min(currentPage * pageSize, totalCustomerCount)} von {totalCustomerCount} Kunden -
- -
- - - - - Seite {currentPage} von {totalCustomerPages} - - - - -
+ {/* Collapsible Bank Details Section */} +
+
- )} - - )} + {showBankDetails ? ( + + ) : ( + + )} + - {/* ─── MODUS B: NEUEN KUNDEN ANLEGEN ─── */} - {customerMode === 'create' && ( - - {( - [ - { label: 'Firmenname *', key: 'company_name', span: true, placeholder: 'GmbH / Einzelunternehmen' }, - { label: 'USt-IdNr.', key: 'vat_id', span: false, placeholder: 'DE123456789' }, - { 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 }) => ( -
- - - setNewCustomerForm((prev: any) => ({ ...prev, [key]: e.target.value })) - } - placeholder={placeholder} - className="bg-white/5 border-white/10 text-white placeholder:text-slate-500" - /> + + {showBankDetails && ( + +
+ + + 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" + /> +
+ +
+ + + 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" + /> +
+ +
+
+ + + 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" + /> +
+ +
+ + + 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" + /> +
+
+
+ )} +
- ))} -
- + + + {/* Drawer Footer */} +
+
- )} - -
- - - - - +
+
+ )} + + ) } diff --git a/shop/components/wizard/step-software.tsx b/shop/components/wizard/step-software.tsx index e91ea75..7b73783 100644 --- a/shop/components/wizard/step-software.tsx +++ b/shop/components/wizard/step-software.tsx @@ -2,13 +2,13 @@ import React from 'react' import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card' -import { Separator } from '@/components/ui/separator' import { Checkbox } from '@/components/ui/checkbox' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { Label } from '@/components/ui/label' import { Input } from '@/components/ui/input' 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 { motion, AnimatePresence } from 'framer-motion' import { Category, Product, CategorySelection } from '@/lib/types' @@ -30,6 +30,8 @@ interface StepSoftwareProps { existingModuleIds?: string[] activeCategoryId: string | null editingDeviceName?: string | null + onCancelEdit?: () => void + productValidationErrors?: string[] } export function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) { @@ -54,6 +56,8 @@ export function StepSoftware({ existingModuleIds = [], activeCategoryId, editingDeviceName = null, + onCancelEdit, + productValidationErrors = [], }: StepSoftwareProps) { 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 return ( - - - -
- - Optionen wählen + + {/* ─── FIXED CARD HEADER ─── */} + +
+
+
+ +
+
+ + {currentCategory.name} + + + {currentCategory.description || 'Passen Sie die Konfiguration für diese Kategorie an.'} + +
+ {editingDeviceName && ( - - ✏️ Bearbeite Kasse: "{editingDeviceName}" - +
+ + ✏️ Bearbeite Kasse: "{editingDeviceName}" + + {onCancelEdit && ( + + )} +
)} - - - Passen Sie die Konfiguration für {currentCategory.name} an. - +
- + + {/* ─── CONTENT AREA (Inherits scrolling from parent, no nested double scrollbar) ─── */} + - {/* Category header */} + {/* Category selection status header */} {(() => { const isMissingRequired = currentCategory.is_required && !sel?.productId && (!sel?.productIds || sel.productIds.length === 0) return ( -
-
-
-

- {currentCategory.name} +
+

+ {currentCategory.name} {isMissingRequired && ( PFLICHTFELD AUSWÄHLEN! )}

- {currentCategory.description && ( -

{currentCategory.description}

- )} +

+ {currentCategory.allow_multiselect ? 'Mehrfachauswahl möglich' : 'Einzelauswahl'} +

{sel?.productIds && sel.productIds.length > 0 ? ( - + {sel.productIds.length} Ausgewählt ) : sel?.productId ? ( - + Ausgewählt ) : currentCategory.is_required ? ( - + Bitte auswählen! ) : ( - + Optional )} @@ -144,12 +170,13 @@ export function StepSoftware({ ) })()} + {/* Products List */} {catProducts.length === 0 ? ( -

+

Keine Produkte in dieser Kategorie im aktuellen Abrechnungsmodell vorhanden.

) : currentCategory.allow_multiselect ? ( -
+
{catProducts.map(product => { const isChecked = sel?.productIds?.includes(product.id) ?? false const disabled = isProductDisabled(product, currentCategory.id) @@ -157,27 +184,27 @@ export function StepSoftware({