diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index ccf269c..0ee17cf 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -1,21 +1,20 @@ 'use client' -import { useState, useMemo } from 'react' +import React, { useState, useEffect, useMemo } from 'react' import { useRouter } from 'next/navigation' -import { motion, AnimatePresence } from 'framer-motion' +import { AnimatePresence, motion } from 'framer-motion' import { Product, ProductModule, Profile, EndCustomer, Order } from '@/lib/types' -import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card' -import { Button } from '@/components/ui/button' -import { Checkbox } from '@/components/ui/checkbox' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Separator } from '@/components/ui/separator' -import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' -import { Check, ChevronRight, ChevronLeft, ShoppingCart, User, ShieldCheck, Cloud, Utensils, HardDrive, Package, AlertCircle, Loader2, UserPlus, Building2, CreditCard, Calendar, Search, Pencil, Trash2 } from 'lucide-react' import { submitOrder, updateOrder } from '@/lib/actions/orders' import { createEndCustomer } from '@/lib/actions/end-customers' import { Category } from '@/lib/types' -import { Badge } from '@/components/ui/badge' + +// Modular Step Components +import { ProgressStepper } from './wizard/progress-stepper' +import { ToastNotification } from './wizard/toast-notification' +import { StepCustomer } from './wizard/step-customer' +import { StepBilling } from './wizard/step-billing' +import { StepSoftware } from './wizard/step-software' +import { StepSummary } from './wizard/step-summary' type CategorySelection = { productId: string | null // selected product in this category @@ -23,7 +22,6 @@ type CategorySelection = { moduleIds: string[] // selected module IDs for this product } -// ─── Billing interval helpers ───────────────────────────────────────────────── function billingLabel(interval?: string) { if (interval === 'one_time') return 'einmalig' return '/ Monat' @@ -34,13 +32,6 @@ function billingBadgeClass(interval?: string) { return 'bg-blue-500/20 text-blue-400 border-blue-500/30' } -// ─── Icon helper ────────────────────────────────────────────────────────────── -function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) { - const Icon = icon === 'Cloud' ? Cloud : icon === 'Utensils' ? Utensils : icon === 'HardDrive' ? HardDrive : Package - return -} - -// ─── Module toggle logic ─────────────────────────────────────────────────────── function toggleModuleInList( modules: ProductModule[], currentIds: string[], @@ -48,7 +39,6 @@ function toggleModuleInList( ): string[] { const isSelected = currentIds.includes(toggleId) if (isSelected) { - // Recursive removal: repeatedly filter until no more dependent modules are removed let next = currentIds.filter(id => id !== toggleId) let changed = true while (changed) { @@ -74,7 +64,6 @@ function isModuleDisabled(module: ProductModule, selectedModules: string[]): boo return !requirementsMet || !!isExcluded } -// ─── Main Wizard ────────────────────────────────────────────────────────────── export function OrderWizard({ products, categories, @@ -171,149 +160,66 @@ export function OrderWizard({ const selectedEndCustomer = endCustomers.find(c => c.id === selectedEndCustomerId) ?? null const [searchTerm, setSearchTerm] = useState('') - - const filteredEndCustomers = useMemo(() => { - const activeCustomers = endCustomers.filter(c => !c.is_anonymized) - const term = searchTerm.toLowerCase().trim() - if (!term) return activeCustomers - - return activeCustomers.filter(customer => { - const company = customer.company_name?.toLowerCase() || '' - const firstName = customer.first_name?.toLowerCase() || '' - const lastName = customer.last_name?.toLowerCase() || '' - const street = customer.street?.toLowerCase() || '' - const zip = customer.zip?.toLowerCase() || '' - const city = customer.city?.toLowerCase() || '' - - return ( - company.includes(term) || - firstName.includes(term) || - lastName.includes(term) || - street.includes(term) || - zip.includes(term) || - city.includes(term) - ) - }) - }, [endCustomers, searchTerm]) - - // Modus: 'select' = Bestandskunde wählen | 'create' = Neuen anlegen const [customerMode, setCustomerMode] = useState<'select' | 'create'>('select') - const [isCreatingCustomer, setIsCreatingCustomer] = useState(false) + + // Formular-State für neuen Endkunden const [newCustomerForm, setNewCustomerForm] = useState({ company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '', bank_iban: '', bank_bic: '', bank_name: '', bank_owner: '', }) + const [isCreatingCustomer, setIsCreatingCustomer] = useState(false) - // Abrechnungsmodell-Auswahl ('one_time' = Kauf, 'monthly' = Abo) - const [selectedBillingInterval, setSelectedBillingInterval] = useState<'one_time' | 'monthly'>( - initialOrder?.order_data?.billing_cycle ?? 'monthly' - ) - - // Modulmengen (moduleId -> Menge) - const [moduleQuantities, setModuleQuantities] = useState>(() => { - if (!initialOrder) return {} - const quantities: Record = {} - initialOrder.order_data?.items?.forEach(item => { - item.selected_modules?.forEach(mod => { - quantities[mod.module_id] = mod.quantity ?? 1 - }) - }) - return quantities + // Standard-Abrechnungsmodell: falls initialOrder vorhanden, dessen Intervall nehmen + const [selectedBillingInterval, setSelectedBillingInterval] = useState<'one_time' | 'monthly'>(() => { + if (initialOrder?.order_data?.items?.[0]?.billing_interval) { + return initialOrder.order_data.items[0].billing_interval as 'one_time' | 'monthly' + } + return 'one_time' }) - // Datum der letzten Lizenz (Bestandskunden-Update-Logik) + // Falls "one_time" (Kauf) gewählt: optionales Datum der letzten CAS-Lizenzierung const [lastLicenseDate, setLastLicenseDate] = useState( - initialOrder?.order_data?.last_license_date ?? "" + initialOrder?.order_data?.last_license_date || '' ) - const lastLicenseMonths = useMemo(() => { - if (!lastLicenseDate || customerMode !== 'select' || !selectedEndCustomerId) return null - const lastDate = new Date(lastLicenseDate) - const today = new Date() - const yearsDiff = today.getFullYear() - lastDate.getFullYear() - const monthsDiff = today.getMonth() - lastDate.getMonth() - return yearsDiff * 12 + monthsDiff - }, [lastLicenseDate, customerMode, selectedEndCustomerId]) - - const updatePriceModifier = useMemo(() => { - if (lastLicenseMonths === null || selectedBillingInterval === 'monthly') { - return { multiplier: 1, discount: 0, label: null } - } - if (lastLicenseMonths <= 12) { - return { multiplier: 0, discount: 1, label: "Update (0-12 Mon.): 100% Rabatt (inklusive)" } - } else if (lastLicenseMonths <= 14) { - return { multiplier: 0.15, discount: 0.85, label: "Update (13-14 Mon.): 15% vom Listenpreis" } - } else if (lastLicenseMonths <= 25) { - return { multiplier: 0.30, discount: 0.70, label: "Update (15-25 Mon.): 30% vom Listenpreis" } - } else if (lastLicenseMonths <= 37) { - return { multiplier: 0.45, discount: 0.55, label: "Update (26-37 Mon.): 45% vom Listenpreis" } - } else if (lastLicenseMonths <= 49) { - return { multiplier: 0.60, discount: 0.40, label: "Update (38-49 Mon.): 60% vom Listenpreis" } - } else { - return { multiplier: 0.90, discount: 0.10, label: "Neukauf (>= 50 Mon.): 10% Rabatt" } - } - }, [lastLicenseMonths, selectedBillingInterval]) - - const visibleCategories = useMemo(() => { - return categories.filter(cat => - selectedBillingInterval === 'one_time' - ? cat.show_in_kauf !== false - : cat.show_in_abo !== false - ) - }, [categories, selectedBillingInterval]) - - const isProductDisabled = (product: Product, currentCategoryId: string) => { - const baseCategoryId = visibleCategories[0]?.id - if (!baseCategoryId || currentCategoryId === baseCategoryId) return false - - const baseProductId = selections[baseCategoryId]?.productId - if (!baseProductId) return false - - const baseProduct = products.find(p => p.id === baseProductId) - if (!baseProduct) return false - - return baseProduct.exclusions?.includes(product.id) || product.exclusions?.includes(baseProductId) - } - - // Per-category selection map: categoryId -> { productId, productIds, moduleIds } + // Aktive Auswahllisten initialisieren const [selections, setSelections] = useState>(() => { - const initInterval = initialOrder?.order_data?.billing_cycle ?? 'monthly' - const init: Record = {} + const initialSels: Record = {} categories.forEach(cat => { - const isVisible = initInterval === 'one_time' ? cat.show_in_kauf !== false : cat.show_in_abo !== false + const isVisible = selectedBillingInterval === 'one_time' ? cat.show_in_kauf !== false : cat.show_in_abo !== false if (!isVisible || cat.allow_multiselect || cat.preselect === false) { - init[cat.id] = { productId: null, productIds: [], moduleIds: [] } + initialSels[cat.id] = { productId: null, productIds: [], moduleIds: [] } } else { - const first = products.find(p => p.category_id === cat.id && (initInterval === 'one_time' ? p.show_in_kauf !== false : p.show_in_abo !== false)) - init[cat.id] = { productId: first?.id ?? null, productIds: first ? [first.id] : [], moduleIds: [] } + const first = products.find(p => p.category_id === cat.id && (selectedBillingInterval === 'one_time' ? p.show_in_kauf !== false : p.show_in_abo !== false)) + initialSels[cat.id] = { productId: first?.id ?? null, productIds: first ? [first.id] : [], moduleIds: [] } } }) - return init + return initialSels }) + const [moduleQuantities, setModuleQuantities] = useState>({}) - // Only REQUIRED categories must have a product selected (which must be filtered by selected branch) - const allCategoriesFilled = useMemo(() => { - return visibleCategories - .filter(cat => cat.is_required) - .every(cat => { - const sel = selections[cat.id] - if (cat.allow_multiselect) { - if (!sel?.productIds || sel.productIds.length === 0) return false - return sel.productIds.every(pId => { - const prod = products.find(p => p.id === pId) - if (!prod) return false - return selectedBillingInterval === 'one_time' ? prod.show_in_kauf !== false : prod.show_in_abo !== false - }) - } - if (!sel?.productId) return false - const prod = products.find(p => p.id === sel.productId) - if (!prod) return false - // Ensure the selected product is actually allowed in the current branch - return selectedBillingInterval === 'one_time' - ? prod.show_in_kauf !== false - : prod.show_in_abo !== false + // Filtere sichtbare Kategorien nach Abrechnungsmodell + const visibleCategories = useMemo(() => { + return categories.filter(cat => { + return selectedBillingInterval === 'one_time' ? cat.show_in_kauf !== false : cat.show_in_abo !== false + }) + }, [categories, selectedBillingInterval]) + + // Endkunden Filterung + const filteredEndCustomers = useMemo(() => { + const term = searchTerm.toLowerCase().trim() + return endCustomers + .filter(c => !c.is_anonymized) + .filter(c => { + if (!term) return true + return ( + c.company_name?.toLowerCase().includes(term) || + c.city?.toLowerCase().includes(term) || + c.zip?.toLowerCase().includes(term) || + `${c.first_name} ${c.last_name}`.toLowerCase().includes(term) + ) }) - }, [visibleCategories, selections, products, selectedBillingInterval]) + }, [endCustomers, searchTerm]) const hasActiveSelection = useMemo(() => { return Object.values(selections).some(sel => sel.productId || (sel.productIds && sel.productIds.length > 0)) @@ -374,7 +280,7 @@ export function OrderWizard({ return errors }, [selections, products, deviceName, basketItems, editingIdx]) - const isNextStepDisabled = !allCategoriesFilled || productValidationErrors.length > 0 + // Total price calculations (split by billing interval) const { monthlyTotal, oneTimeTotal } = useMemo(() => { @@ -395,36 +301,59 @@ export function OrderWizard({ if (p) selectedProds.push(p) } - if (selectedProds.length === 0) continue - - // Apply free items limit: sort selected products by base_price ascending so that the cheapest are free - const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price) const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0 + const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price) - sortedProds.forEach((prod, sortedIndex) => { - const base = sortedIndex < freeLimit ? 0 : Number(prod.base_price) - let modulesSum = 0 + sortedProds.forEach((prod, idx) => { + const isFree = idx < freeLimit + const basePrice = isFree ? 0 : prod.base_price + + if (prod.billing_interval === 'monthly') { + monthly += basePrice + } else { + oneTime += basePrice + } + + // Add module prices sel.moduleIds.forEach(mId => { const mod = prod.modules?.find(m => m.id === mId) if (mod) { const qty = moduleQuantities[mId] || 1 - modulesSum += Number(mod.price) * qty + if (prod.billing_interval === 'monthly') { + monthly += mod.price * qty + } else { + oneTime += mod.price * qty + } } }) - const totalItem = base + modulesSum - if (prod.billing_interval === 'one_time') { - const allowDiscount = (prod as any).allow_update_discount !== false - const finalItemTotal = allowDiscount ? (totalItem * updatePriceModifier.multiplier) : totalItem - oneTime += finalItemTotal - } else { - monthly += totalItem - } }) } return { monthlyTotal: monthly, oneTimeTotal: oneTime } - }, [selections, products, categories, moduleQuantities, updatePriceModifier]) + }, [selections, products, categories, moduleQuantities]) - const oneTimeNet = oneTimeTotal + // Ermittlung der Update-Faktoren basierend auf dem Lizenzdatum (nur für Kauf/einmalig relevant) + const updatePriceModifier = useMemo(() => { + if (selectedBillingInterval !== 'one_time' || !lastLicenseDate) return { factor: 1 } + + const lastDate = new Date(lastLicenseDate) + const now = new Date() + const diffTime = Math.abs(now.getTime() - lastDate.getTime()) + const diffMonths = Math.ceil(diffTime / (1000 * 60 * 60 * 24 * 30)) + + if (diffMonths <= 12) { + return { factor: 0.15, label: 'Update innerhalb 1 Jahr (15% des Neupreises)', diffMonths } + } else if (diffMonths <= 24) { + return { factor: 0.30, label: 'Update innerhalb 2 Jahre (30% des Neupreises)', diffMonths } + } else if (diffMonths <= 36) { + return { factor: 0.50, label: 'Update innerhalb 3 Jahre (50% des Neupreises)', diffMonths } + } else { + return { factor: 1.0, label: 'Älter als 3 Jahre: Voller Neupreis', diffMonths } + } + }, [selectedBillingInterval, lastLicenseDate]) + + // Endbeträge ermitteln (Netto / Steuer / Brutto) + const discountFactor = updatePriceModifier.factor ?? 1 + const oneTimeNet = oneTimeTotal * discountFactor const oneTimeTax = oneTimeNet * 0.19 const oneTimeGross = oneTimeNet + oneTimeTax @@ -432,118 +361,35 @@ export function OrderWizard({ const monthlyTax = monthlyNet * 0.19 const monthlyGross = monthlyNet + monthlyTax - // Helper: update product selection for a category - function selectProduct(catId: string, productId: string) { - const cat = categories.find(c => c.id === catId) - setSelections(prev => { - const sel = prev[catId] - let nextProductIds = sel?.productIds || [] - let nextProductId = sel?.productId || null - - if (cat?.allow_multiselect) { - if (nextProductIds.includes(productId)) { - nextProductIds = nextProductIds.filter(id => id !== productId) - } else { - nextProductIds = [...nextProductIds, productId] - } - nextProductId = nextProductIds[0] || null - } else { - nextProductIds = [productId] - nextProductId = productId - } - - const isSelectedNow = cat?.allow_multiselect - ? nextProductIds.includes(productId) - : nextProductId === productId - - const shouldReset = isSelectedNow && cat?.resets_others - - const next = { - ...prev, - [catId]: { productId: nextProductId, productIds: nextProductIds, moduleIds: [] }, - } - - // Kaskaden-Bereinigung: Entferne inkompatible Module in anderen Kategorien - const newProduct = products.find(p => p.id === nextProductId) - categories.forEach(c => { - if (c.id === catId) return - const s = next[c.id] - if (s && s.moduleIds.length > 0) { - s.moduleIds = s.moduleIds.filter(mId => { - // Prüfe ob das neue Basis-Produkt dieses Modul ausschließt - const isExcluded = newProduct?.exclusions?.includes(mId) - return !isExcluded + // Only REQUIRED categories must have a product selected + const allCategoriesFilled = useMemo(() => { + return visibleCategories + .filter(cat => cat.is_required) + .every(cat => { + const sel = selections[cat.id] + if (cat.allow_multiselect) { + if (!sel?.productIds || sel.productIds.length === 0) return false + return sel.productIds.every(pId => { + const prod = products.find(p => p.id === pId) + if (!prod) return false + return selectedBillingInterval === 'one_time' ? prod.show_in_kauf !== false : prod.show_in_abo !== false }) } + if (!sel?.productId) return false + const prod = products.find(p => p.id === sel.productId) + if (!prod) return false + return selectedBillingInterval === 'one_time' + ? prod.show_in_kauf !== false + : prod.show_in_abo !== false }) + }, [visibleCategories, selections, products, selectedBillingInterval]) - if (shouldReset) { - // Reset all other categories - categories.forEach(c => { - if (c.id !== catId) { - next[c.id] = { productId: null, productIds: [], moduleIds: [] } - } - }) - } else { - // Automatically deselect now conflicting products in other categories - const selectedIds: string[] = [] - Object.values(next).forEach(s => { - if (s.productIds && s.productIds.length > 0) { - s.productIds.forEach(id => { - if (id) selectedIds.push(id) - }) - } else if (s.productId) { - selectedIds.push(s.productId) - } - }) - - categories.forEach(c => { - if (c.id === catId) return - const s = next[c.id] - if (c.allow_multiselect && s?.productIds) { - const validProductIds = s.productIds.filter(pId => { - const prod = products.find(p => p.id === pId) - const otherSelectedProductIds = selectedIds.filter(id => id !== pId) - const isExcluded = prod && ( - otherSelectedProductIds.some(otherId => { - const otherProd = products.find(p => p.id === otherId) - return otherProd?.exclusions?.includes(prod.id) - }) || - prod.exclusions?.some(exId => otherSelectedProductIds.includes(exId)) - ) - return !isExcluded - }) - next[c.id] = { - productId: validProductIds[0] || null, - productIds: validProductIds, - moduleIds: s.moduleIds - } - } else if (s?.productId) { - const prod = products.find(p => p.id === s.productId) - const otherSelectedProductIds = selectedIds.filter(id => id !== s.productId) - const isExcluded = prod && ( - otherSelectedProductIds.some(otherId => { - const otherProd = products.find(p => p.id === otherId) - return otherProd?.exclusions?.includes(prod.id) - }) || - prod.exclusions?.some(exId => otherSelectedProductIds.includes(exId)) - ) - if (isExcluded) { - next[c.id] = { productId: null, productIds: [], moduleIds: [] } - } - } - }) - } - - return next - }) - } + const isNextStepDisabled = !allCategoriesFilled || productValidationErrors.length > 0 // Helper: toggle module for a category's selected product function toggleModule(catId: string, moduleId: string) { setSelections(prev => { const sel = prev[catId] - // Pick first selected product to associate module with (or fall back to single select productId) const activeProductId = sel?.productId if (!activeProductId) return prev const prod = products.find(p => p.id === activeProductId) @@ -567,14 +413,15 @@ export function OrderWizard({ } setStep(s => s + 1) } - const prevStep = () => setStep(s => s - 1) - // Abrechnungsmodell wechseln (und Selektionen der Kategorien anpassen) - const handleBillingIntervalChange = (val: 'one_time' | 'monthly') => { + const prevStep = () => { + setStep(s => s - 1) + } + + function handleBillingIntervalChange(val: 'one_time' | 'monthly') { setSelectedBillingInterval(val) - setModuleQuantities({}) if (val === 'monthly') { - setLastLicenseDate("") + setLastLicenseDate('') } const newSelections: Record = {} categories.forEach(cat => { @@ -635,10 +482,9 @@ export function OrderWizard({ setBasketItems(prev => [...prev, currentItem]) } - setModuleQuantities({}) + // Reset current form config setDeviceName('') - - // Reset selections + setModuleQuantities({}) const resetSels: Record = {} categories.forEach(cat => { const isVisible = selectedBillingInterval === 'one_time' ? cat.show_in_kauf !== false : cat.show_in_abo !== false @@ -711,7 +557,9 @@ export function OrderWizard({ const res = await fetch('/api/orders/checkout', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + }, body: JSON.stringify({ items: finalItems, customerProfile: customerData, @@ -736,46 +584,70 @@ export function OrderWizard({ } } - const finalItemsToShow = basketItems.length > 0 ? basketItems : (allCategoriesFilled && productValidationErrors.length === 0 ? [{ deviceName: deviceName || 'Kasse 1', selections, moduleQuantities, billingInterval: selectedBillingInterval }] - : []); + : []) + + function selectProduct(catId: string, productId: string) { + setSelections(prev => { + const cat = categories.find(c => c.id === catId) + if (cat?.allow_multiselect) { + const currentIds = prev[catId]?.productIds || [] + const nextIds = currentIds.includes(productId) + ? currentIds.filter(id => id !== productId) + : [...currentIds, productId] + return { + ...prev, + [catId]: { + productId: nextIds[0] || null, + productIds: nextIds, + moduleIds: prev[catId]?.moduleIds || [] + } + } + } else { + const isCurrentlyChecked = prev[catId]?.productId === productId + const nextId = isCurrentlyChecked ? null : productId + return { + ...prev, + [catId]: { + productId: nextId, + productIds: nextId ? [nextId] : [], + moduleIds: [] + } + } + } + }) + } + + function isProductDisabled(product: Product, catId: string): boolean { + const otherSelectedProductIds: string[] = [] + Object.entries(selections).forEach(([cId, sel]) => { + if (cId === catId) return + if (sel.productIds && sel.productIds.length > 0) { + otherSelectedProductIds.push(...sel.productIds) + } else if (sel.productId) { + otherSelectedProductIds.push(sel.productId) + } + }) + + const hasExclusions = product.exclusions?.some(exId => otherSelectedProductIds.includes(exId)) + const meetsRequirements = !product.requirements?.length || + product.requirements.some(reqId => otherSelectedProductIds.includes(reqId)) + + return !!hasExclusions || !meetsRequirements + } return (
{/* Progress Stepper */} {step !== 3 && ( -
-
-
- {[1, 2, 3, 4].map(s => ( -
-
= s - ? 'bg-primary text-white scale-110 shadow-[0_0_15px_rgba(59,130,246,0.5)]' - : 'bg-slate-800 text-slate-400' - }`} - > - {step > s ? : s} -
- = s ? 'text-primary' : 'text-slate-500'} flex items-center gap-1`}> - {s === 1 ? 'Kunde' : s === 2 ? 'Modell' : s === 3 ? 'Software' : 'Abschluss'} - {s === 3 && basketItems.length > 0 && ( - - {basketItems.length} - - )} - -
- ))} -
-
+ )} - {/* ───── Schritt 1: Endkunde wählen / anlegen ───── */} + {/* Step 1: Customer */} {step === 1 && ( - - - - - Für wen bestellen Sie? - - - Wählen Sie einen Endkunden aus oder legen Sie einen neuen an. - - - - {/* Admin Company Selector */} - {isAdmin && ( -
- - -
- )} - - {/* Modus-Toggle */} -
- - -
- - {/* Modus A: Bestandskunde wählen */} - {customerMode === 'select' && ( -
- {endCustomers.filter(c => !c.is_anonymized).length === 0 ? ( -
- -

Noch keine Endkunden angelegt.

- -
- ) : ( -
-
- - setSearchTerm(e.target.value)} - className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-500" - /> -
- {filteredEndCustomers.length === 0 ? ( -
-

Keine passenden Kunden gefunden.

-
- ) : ( -
- {filteredEndCustomers.map(customer => ( - - ))} -
- )} -
- )} -
- )} - - {/* 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 => ({ ...prev, [key]: e.target.value }))} - placeholder={placeholder} - className="bg-white/5 border-white/10 text-white placeholder:text-slate-500" - /> -
- ))} -
- - -
-
- )} -
- - - -
+
)} - {/* ───── Schritt 2: Abrechnungsmodell wählen ───── */} + {/* Step 2: Billing Model */} {step === 2 && ( - - - - - Abrechnungsmodell wählen - - - Möchten Sie Lizenzen dauerhaft kaufen oder monatlich mieten? - - - -
- {/* 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 h-48 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' - }`} - > -
-
-
- -
-

Einmaliger Kauf

-
-

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

-
- {selectedBillingInterval === 'one_time' && ( -
- -
- )} -
- - {/* Option 2: Abo */} -
handleBillingIntervalChange('monthly')} - className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between h-48 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' - }`} - > -
-
-
- -
-

Monatliches Abo (Miete)

-
-

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

-
- {selectedBillingInterval === 'monthly' && ( -
- -
- )} -
-
- {selectedBillingInterval === 'one_time' && customerMode === 'select' && selectedEndCustomerId && ( -
- - setLastLicenseDate(e.target.value)} - className="bg-[#0b1329] border-white/10 text-white focus:border-primary" - /> -

- Falls dieser Kunde bereits Lizenzen besitzt, tragen Sie das Datum der letzten Lizenzierung ein. Damit werden die korrekten Update-Gebühren ermittelt. -

-
- )} -
- - - - -
+
)} - {/* ───── Schritt 3: Software wählen ───── */} + {/* Step 3: Software Selector */} {step === 3 && ( -
- {/* Left: Category sections */} -
- {/* Progress Stepper docked above Software wählen */} -
-
-
- {[1, 2, 3, 4].map(s => ( -
-
= s - ? 'bg-primary text-white scale-110 shadow-[0_0_15px_rgba(59,130,246,0.5)]' - : 'bg-slate-800 text-slate-400' - }`} - > - {step > s ? : s} -
- = s ? 'text-primary' : 'text-slate-500'} flex items-center gap-1`}> - {s === 1 ? 'Kunde' : s === 2 ? 'Modell' : s === 3 ? 'Software' : 'Abschluss'} - {s === 3 && basketItems.length > 0 && ( - - {basketItems.length} - - )} - -
- ))} -
-
- - - - - - Software wählen - - - Wählen Sie pro Kategorie mindestens einen Artikel aus. - - - - {visibleCategories.map((cat, idx) => { - const otherSelectedProductIds = Object.entries(selections) - .filter(([cId]) => cId !== cat.id) - .map(([, s]) => s.productId) - .filter((id): id is string => !!id) - - // Filter products based on display flags (do not hide due to exclusions to prevent UI deadlocks) - const catProducts = products.filter(p => { - if (p.category_id !== cat.id) return false - - const matchesInterval = selectedBillingInterval === 'one_time' - ? p.show_in_kauf !== false - : p.show_in_abo !== false - return matchesInterval - }) - const sel = selections[cat.id] - const selectedProduct = catProducts.find(p => p.id === sel?.productId) ?? null - - return ( -
- {idx > 0 && } - - {/* Category header */} -
-
- -
-
-

{cat.name}

- {cat.description && ( -

{cat.description}

- )} -
- {sel?.productIds && sel.productIds.length > 0 ? ( - - {sel.productIds.length} Ausgewählt - - ) : sel?.productId ? ( - - Ausgewählt - - ) : cat.is_required ? ( - - Pflichtfeld - - ) : ( - - Optional - - )} -
- - {catProducts.length === 0 ? ( -

- Keine Produkte in dieser Kategorie im aktuellen Abrechnungsmodell vorhanden. -

- ) : cat.allow_multiselect ? ( -
- {catProducts.map(product => { - const isChecked = sel?.productIds?.includes(product.id) ?? false - const disabled = isProductDisabled(product, cat.id) - return ( -
- -
- ) - })} -
- ) : ( - selectProduct(cat.id, id)} - className="grid gap-3" - > - {catProducts.map(product => { - const disabled = isProductDisabled(product, cat.id) - return ( -
- - -
- ) - })} -
- )} - - {/* Modules for selected product in this category */} - {selectedProduct?.modules && selectedProduct.modules.length > 0 && ( -
-

Zusatzmodule:

- {selectedProduct.modules.map(module => { - const disabled = isModuleDisabled(module, sel?.moduleIds ?? []) - const checked = sel?.moduleIds.includes(module.id) ?? false - return ( -
-
- toggleModule(cat.id, module.id)} - disabled={disabled} - /> -
- - {module.description && ( -

{module.description}

- )} - {disabled && ( -

- {module.requirements?.length && !module.requirements.some( - reqId => sel?.moduleIds.includes(reqId) - ) - ? 'Benötigt weitere Module' - : 'Nicht kombinierbar mit aktueller Auswahl'} -

- )} -
-
- - {/* Scalable Quantity Input */} - {checked && module.has_quantity && ( -
- - { - const val = Math.max(1, parseInt(e.target.value) || 1) - setModuleQuantities(prev => ({ ...prev, [module.id]: val })) - }} - className="w-20 h-8 bg-white/5 border-white/10 text-white text-xs text-center rounded-lg" - /> - - Gesamt: {new Intl.NumberFormat('de-DE', { - style: 'currency', - currency: 'EUR', - }).format(module.price * (moduleQuantities[module.id] || 1))} - -
- )} -
- ) - })} -
- )} -
- ) - })} -
-
-
- - {/* Right: Summary */} -
- - - Zusammenfassung - - - {visibleCategories.map(cat => { - const sel = selections[cat.id] - const selectedProds: Product[] = [] - if (cat.allow_multiselect && sel?.productIds) { - sel.productIds.forEach(pId => { - const p = products.find(prod => prod.id === pId) - if (p) selectedProds.push(p) - }) - } else if (sel?.productId) { - const p = products.find(prod => prod.id === sel.productId) - if (p) selectedProds.push(p) - } - - if (selectedProds.length === 0) return ( -
- {cat.is_required ? ( - - ) : ( - - )} - {cat.name}: {cat.is_required ? 'nicht gewählt' : 'nicht gewählt (optional)'} -
- ) - - // Sort selected products by price to determine which ones are free - const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price) - const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0 - - return ( -
-
- {cat.name} -
- {sortedProds.map((prod, idx) => { - const isFree = idx < freeLimit - const actualPrice = isFree ? 0 : prod.base_price - return ( -
-
- {prod.name} {isFree && (Frei)} - - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(actualPrice)} - {' '}{billingLabel(prod.billing_interval)} - -
- {sel.moduleIds.map(mId => { - const mod = prod.modules?.find(m => m.id === mId) - const qty = moduleQuantities[mId] || 1 - return mod ? ( -
- + {mod.name} {mod.has_quantity ? `(x${qty})` : ''} - - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price * qty)} - -
- ) : null - })} -
- ) - })} -
- ) - })} - - {oneTimeTotal > 0 && monthlyTotal > 0 ? ( -
-
-

Einmalig:

-
- Netto: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)} -
-
- zzgl. 19% MwSt.: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)} -
-
- Gesamt (brutto): - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)} -
-
-
-

Monatlich:

-
- Netto: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl. -
-
- zzgl. 19% MwSt.: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl. -
-
- Gesamt (brutto): - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl. -
-
-
- ) : oneTimeTotal > 0 ? ( -
-
- Netto-Gesamtbetrag: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)} -
-
- zzgl. 19% MwSt.: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)} -
-
- Gesamtbetrag (brutto): - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)} -
-
- ) : ( -
-
- Netto-Gesamtbetrag: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl. -
-
- zzgl. 19% MwSt.: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl. -
-
- Gesamtbetrag (brutto): - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl. -
-
- )} - {updatePriceModifier.label && ( -
-

- - Update-Rabatt aktiv -

-

{updatePriceModifier.label}

-
- )} - {!allCategoriesFilled && ( -

- - Bitte aus jeder Pflichtkategorie einen Artikel wählen. -

- )} - {productValidationErrors.map((err, errIdx) => ( -

- - {err} -

- ))} - {basketItems.length > 0 && ( -
-

Warenkorb ({basketItems.length}):

-
- {basketItems.map((item, idx) => ( -
-
- {item.deviceName} - {item.billingInterval === 'one_time' ? 'Kauf' : 'Abo'} -
-
- - -
-
- ))} -
-
- )} -
- -
- - setDeviceName(e.target.value)} - className="bg-white/5 border-white/10 text-white text-xs h-9 rounded-lg" - /> -
- - - - -
-
-
-
+ + )} - {/* ───── Schritt 4: Prüfung & Abschluss ───── */} + {/* Step 4: Verification & Submit */} {step === 4 && ( - - -
- -
- Anfrage prüfen - - Fast fertig! Bitte überprüfen Sie Ihre Auswahl. - -
- -
- {finalItemsToShow.map((item, itemIdx) => ( -
-
- Kasse: {item.deviceName} - {item.billingInterval === 'one_time' ? 'Kauf' : 'Abo'} -
- - {visibleCategories.map(cat => { - const sel = item.selections[cat.id] - const selectedProds: Product[] = [] - if (cat.allow_multiselect && sel?.productIds) { - sel.productIds.forEach((pId: string) => { - const p = products.find(prod => prod.id === pId) - if (p) selectedProds.push(p) - }) - } else if (sel?.productId) { - const p = products.find(prod => prod.id === sel.productId) - if (p) selectedProds.push(p) - } - - if (selectedProds.length === 0) return null - - const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price) - const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0 - - return ( -
-
- - {cat.name} -
- {sortedProds.map((prod, idx) => { - const isFree = idx < freeLimit - const actualPrice = isFree ? 0 : prod.base_price - const catTotal = - Number(actualPrice) + - (sel?.moduleIds?.reduce((acc: number, mId: string) => { - const mod = prod.modules?.find(m => m.id === mId) - const qty = item.moduleQuantities[mId] || 1 - return acc + (Number(mod?.price ?? 0) * qty) - }, 0) || 0) - - return ( -
-
- - {prod.name} {isFree && (Frei)} - - - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(catTotal)} - -
- {sel?.moduleIds && sel.moduleIds.length > 0 && ( -

- Module: {sel.moduleIds - .map((id: string) => { - const mod = prod.modules?.find(m => m.id === id) - const qty = item.moduleQuantities[id] || 1 - return mod ? `${mod.name}${mod.has_quantity ? ` (x${qty})` : ''}` : '' - }) - .filter(Boolean) - .join(', ')} -

- )} -
- ) - })} -
- ) - })} -
- ))} - -
- {selectedEndCustomer ? ( - <> -

- - {selectedEndCustomer.company_name} -

-

{[selectedEndCustomer.first_name, selectedEndCustomer.last_name].filter(Boolean).join(' ')}

-

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

-

Endkunde Ihres Partners

- {lastLicenseDate && ( -

- - Letzte Lizenz vom: {new Date(lastLicenseDate).toLocaleDateString('de-DE')} -

- )} - - ) : ( - <> -

{customerData.company_name}

-

{customerData.first_name} {customerData.last_name}

-

{customerData.address}, {customerData.zip_code} {customerData.city}

- - )} -
-
- {oneTimeTotal > 0 && monthlyTotal > 0 ? ( -
-
-

Einmaliger Betrag:

-
- Netto: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)} -
-
- zzgl. 19% MwSt.: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)} -
-
- Brutto: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)} -
-
- {updatePriceModifier.label && ( -
- {updatePriceModifier.label} -
- )} -
-

Monatlicher Betrag:

-
- Netto: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl. -
-
- zzgl. 19% MwSt.: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl. -
-
- Brutto: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl. -
-
-
- ) : oneTimeTotal > 0 ? ( -
-
- Netto-Gesamtbetrag: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)} -
-
- zzgl. 19% MwSt.: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)} -
- {updatePriceModifier.label && ( -
- {updatePriceModifier.label} -
- )} -
- Gesamtbetrag (brutto): - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)} -
-
- ) : ( -
-
- Netto-Gesamtbetrag: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl. -
-
- zzgl. 19% MwSt.: - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl. -
-
- Gesamtbetrag (brutto): - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl. -
-
- )} -

- Mit dem Klick auf „Kostenpflichtig bestellen" akzeptieren Sie unsere AGB und die{' '} - - Datenschutzerklärung - . -

-
- - - - -
+
)} - {/* Custom Toast Notification */} - - {toast && ( - - -

{toast.message}

- -
- )} -
+ {/* Toast Notification Container */} + setToast(null)} />
) } diff --git a/shop/components/wizard/progress-stepper.tsx b/shop/components/wizard/progress-stepper.tsx new file mode 100644 index 0000000..9b6e764 --- /dev/null +++ b/shop/components/wizard/progress-stepper.tsx @@ -0,0 +1,39 @@ +'use client' + +import { Check } from 'lucide-react' + +interface ProgressStepperProps { + step: number + basketItemsCount: number +} + +export function ProgressStepper({ step, basketItemsCount }: ProgressStepperProps) { + return ( +
+
+
+ {[1, 2, 3, 4].map(s => ( +
+
= s + ? 'bg-primary text-white scale-110 shadow-[0_0_15px_rgba(59,130,246,0.5)]' + : 'bg-slate-800 text-slate-400' + }`} + > + {step > s ? : s} +
+ = s ? 'text-primary' : 'text-slate-500'} flex items-center gap-1`}> + {s === 1 ? 'Kunde' : s === 2 ? 'Modell' : s === 3 ? 'Software' : 'Abschluss'} + {s === 3 && basketItemsCount > 0 && ( + + {basketItemsCount} + + )} + +
+ ))} +
+
+ ) +} diff --git a/shop/components/wizard/step-billing.tsx b/shop/components/wizard/step-billing.tsx new file mode 100644 index 0000000..5f86aea --- /dev/null +++ b/shop/components/wizard/step-billing.tsx @@ -0,0 +1,127 @@ +'use client' + +import React from 'react' +import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } 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' + +interface StepBillingProps { + selectedBillingInterval: 'one_time' | 'monthly' + handleBillingIntervalChange: (val: 'one_time' | 'monthly') => void + customerMode: 'select' | 'create' + selectedEndCustomerId: string | null + lastLicenseDate: string + setLastLicenseDate: (date: string) => void + prevStep: () => void + nextStep: () => void +} + +export function StepBilling({ + selectedBillingInterval, + handleBillingIntervalChange, + customerMode, + selectedEndCustomerId, + lastLicenseDate, + setLastLicenseDate, + prevStep, + nextStep, +}: StepBillingProps) { + return ( + + + + + Abrechnungsmodell wählen + + + Möchten Sie Lizenzen dauerhaft kaufen oder monatlich mieten? + + + +
+ {/* 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 h-48 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' + }`} + > +
+
+
+ +
+

Einmaliger Kauf

+
+

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

+
+ {selectedBillingInterval === 'one_time' && ( +
+ +
+ )} +
+ + {/* Option 2: Abo */} +
handleBillingIntervalChange('monthly')} + className={`relative p-6 rounded-2xl border-2 cursor-pointer transition-all duration-300 flex flex-col justify-between h-48 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' + }`} + > +
+
+
+ +
+

Monatliches Abo (Miete)

+
+

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

+
+ {selectedBillingInterval === 'monthly' && ( +
+ +
+ )} +
+
+ {selectedBillingInterval === 'one_time' && customerMode === 'select' && selectedEndCustomerId && ( +
+ + setLastLicenseDate(e.target.value)} + className="bg-[#0b1329] border-white/10 text-white focus:border-primary" + /> +

+ Falls dieser Kunde bereits Lizenzen besitzt, tragen Sie das Datum der letzten Lizenzierung ein. Damit werden die korrekten Update-Gebühren ermittelt. +

+
+ )} +
+ + + + +
+ ) +} diff --git a/shop/components/wizard/step-customer.tsx b/shop/components/wizard/step-customer.tsx new file mode 100644 index 0000000..b3ebdd2 --- /dev/null +++ b/shop/components/wizard/step-customer.tsx @@ -0,0 +1,224 @@ +'use client' + +import React from 'react' +import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card' +import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { User, Building2, UserPlus, Search, Check, Loader2, ChevronRight } from 'lucide-react' +import { EndCustomer, Profile } from '@/lib/types' + +interface StepCustomerProps { + isAdmin: boolean + companies: any[] + selectedCompanyId: string | null + setSelectedCompanyId: (id: string | null) => void + customerMode: 'select' | 'create' + setCustomerMode: (mode: 'select' | 'create') => void + endCustomers: EndCustomer[] + searchTerm: string + setSearchTerm: (term: string) => void + filteredEndCustomers: EndCustomer[] + selectedEndCustomerId: string | null + setSelectedEndCustomerId: (id: string | null) => void + newCustomerForm: any + setNewCustomerForm: React.Dispatch> + handleCreateCustomer: () => Promise + isCreatingCustomer: boolean + nextStep: () => void +} + +export function StepCustomer({ + isAdmin, + companies, + selectedCompanyId, + setSelectedCompanyId, + customerMode, + setCustomerMode, + endCustomers, + searchTerm, + setSearchTerm, + filteredEndCustomers, + selectedEndCustomerId, + setSelectedEndCustomerId, + newCustomerForm, + setNewCustomerForm, + handleCreateCustomer, + isCreatingCustomer, + nextStep, +}: StepCustomerProps) { + return ( + + + + + Für wen bestellen Sie? + + + Wählen Sie einen Endkunden aus oder legen Sie einen neuen an. + + + + {/* Admin Company Selector */} + {isAdmin && ( +
+ + +
+ )} + + {/* Modus-Toggle */} +
+ + +
+ + {/* Modus A: Bestandskunde wählen */} + {customerMode === 'select' && ( +
+ {endCustomers.filter(c => !c.is_anonymized).length === 0 ? ( +
+ +

Noch keine Endkunden angelegt.

+ +
+ ) : ( +
+
+ + setSearchTerm(e.target.value)} + className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-500" + /> +
+ {filteredEndCustomers.length === 0 ? ( +
+

Keine passenden Kunden gefunden.

+
+ ) : ( +
+ {filteredEndCustomers.map(customer => ( + + ))} +
+ )} +
+ )} +
+ )} + + {/* 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" + /> +
+ ))} +
+ + +
+
+ )} +
+ + + +
+ ) +} diff --git a/shop/components/wizard/step-software.tsx b/shop/components/wizard/step-software.tsx new file mode 100644 index 0000000..209e793 --- /dev/null +++ b/shop/components/wizard/step-software.tsx @@ -0,0 +1,378 @@ +'use client' + +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 } from 'lucide-react' +import * as Icons from 'lucide-react' +import { Category, Product, CategorySelection } from '@/lib/types' +import { SummarySidebar } from './summary-sidebar' + +interface StepSoftwareProps { + visibleCategories: Category[] + products: Product[] + selections: Record + selectProduct: (catId: string, prodId: string) => void + isProductDisabled: (prod: Product, catId: string) => boolean + isModuleDisabled: (mod: any, selectedModIds: string[]) => boolean + toggleModule: (catId: string, modId: string) => void + moduleQuantities: Record + setModuleQuantities: React.Dispatch>> + selectedBillingInterval: 'one_time' | 'monthly' + billingLabel: (interval: string) => string + billingBadgeClass: (interval: string) => string + + // Sidebar props + oneTimeTotal: number + monthlyTotal: number + oneTimeNet: number + oneTimeTax: number + oneTimeGross: number + monthlyNet: number + monthlyTax: number + monthlyGross: number + updatePriceModifier: { label?: string } + allCategoriesFilled: boolean + productValidationErrors: string[] + basketItems: any[] + editingIdx: number | null + editBasketItem: (idx: number) => void + deleteBasketItem: (idx: number) => void + deviceName: string + setDeviceName: (name: string) => void + addToBasket: () => void + isNextStepDisabled: boolean + hasActiveSelection: boolean + nextStep: () => void + prevStep: () => void +} + +function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) { + const LucideIcon = icon ? (Icons as any)[icon] : null + if (!LucideIcon) return + return +} + +export function StepSoftware({ + visibleCategories, + products, + selections, + selectProduct, + isProductDisabled, + isModuleDisabled, + toggleModule, + moduleQuantities, + setModuleQuantities, + selectedBillingInterval, + billingLabel, + billingBadgeClass, + oneTimeTotal, + monthlyTotal, + oneTimeNet, + oneTimeTax, + oneTimeGross, + monthlyNet, + monthlyTax, + monthlyGross, + updatePriceModifier, + allCategoriesFilled, + productValidationErrors, + basketItems, + editingIdx, + editBasketItem, + deleteBasketItem, + deviceName, + setDeviceName, + addToBasket, + isNextStepDisabled, + hasActiveSelection, + nextStep, + prevStep, +}: StepSoftwareProps) { + return ( +
+ {/* Left: Category sections */} +
+ + + + + Software wählen + + + Wählen Sie pro Kategorie mindestens einen Artikel aus. + + + + {visibleCategories.map((cat, idx) => { + const catProducts = products.filter(p => { + if (p.category_id !== cat.id) return false + return selectedBillingInterval === 'one_time' + ? p.show_in_kauf !== false + : p.show_in_abo !== false + }) + const sel = selections[cat.id] + const selectedProduct = catProducts.find(p => p.id === sel?.productId) ?? null + + return ( +
+ {idx > 0 && } + + {/* Category header */} +
+
+ +
+
+

{cat.name}

+ {cat.description && ( +

{cat.description}

+ )} +
+ {sel?.productIds && sel.productIds.length > 0 ? ( + + {sel.productIds.length} Ausgewählt + + ) : sel?.productId ? ( + + Ausgewählt + + ) : cat.is_required ? ( + + Pflichtfeld + + ) : ( + + Optional + + )} +
+ + {catProducts.length === 0 ? ( +

+ Keine Produkte in dieser Kategorie im aktuellen Abrechnungsmodell vorhanden. +

+ ) : cat.allow_multiselect ? ( +
+ {catProducts.map(product => { + const isChecked = sel?.productIds?.includes(product.id) ?? false + const disabled = isProductDisabled(product, cat.id) + return ( +
+ +
+ ) + })} +
+ ) : ( + selectProduct(cat.id, id)} + className="grid gap-3" + > + {catProducts.map(product => { + const disabled = isProductDisabled(product, cat.id) + return ( +
+ + +
+ ) + })} +
+ )} + + {/* Modules */} + {selectedProduct?.modules && selectedProduct.modules.length > 0 && ( +
+

Zusatzmodule:

+ {selectedProduct.modules.map(module => { + const disabled = isModuleDisabled(module, sel?.moduleIds ?? []) + const checked = sel?.moduleIds.includes(module.id) ?? false + return ( +
+
+ toggleModule(cat.id, module.id)} + disabled={disabled} + /> +
+ + {module.description && ( +

{module.description}

+ )} + {disabled && ( +

+ {module.requirements?.length && !module.requirements.some( + reqId => sel?.moduleIds.includes(reqId) + ) + ? 'Benötigt weitere Module' + : 'Nicht kombinierbar mit aktueller Auswahl'} +

+ )} +
+
+ + {/* Scalable Quantity */} + {checked && module.has_quantity && ( +
+ + { + const val = Math.max(1, parseInt(e.target.value) || 1) + setModuleQuantities(prev => ({ ...prev, [module.id]: val })) + }} + className="w-20 h-8 bg-white/5 border-white/10 text-white text-xs text-center rounded-lg" + /> + + Gesamt: {new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR', + }).format(module.price * (moduleQuantities[module.id] || 1))} + +
+ )} +
+ ) + })} +
+ )} +
+ ) + })} +
+
+
+ + {/* Right: Summary sidebar */} + +
+ ) +} diff --git a/shop/components/wizard/step-summary.tsx b/shop/components/wizard/step-summary.tsx new file mode 100644 index 0000000..b73eeee --- /dev/null +++ b/shop/components/wizard/step-summary.tsx @@ -0,0 +1,275 @@ +'use client' + +import React from 'react' +import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card' +import { Separator } from '@/components/ui/separator' +import { Button } from '@/components/ui/button' +import { ShieldCheck, Building2, Calendar, Loader2 } from 'lucide-react' +import * as Icons from 'lucide-react' +import { Category, Product, EndCustomer, Profile } from '@/lib/types' + +interface StepSummaryProps { + finalItemsToShow: any[] + visibleCategories: Category[] + products: Product[] + oneTimeTotal: number + monthlyTotal: number + oneTimeNet: number + oneTimeTax: number + oneTimeGross: number + monthlyNet: number + monthlyTax: number + monthlyGross: number + updatePriceModifier: { label?: string } + selectedEndCustomer: EndCustomer | null + customerData: Partial + lastLicenseDate: string + handleSubmit: () => Promise + isSubmitting: boolean + initialOrder: any + prevStep: () => void +} + +function CategoryIcon({ icon, className }: { icon?: string | null; className?: string }) { + const LucideIcon = icon ? (Icons as any)[icon] : null + if (!LucideIcon) return + return +} + +export function StepSummary({ + finalItemsToShow, + visibleCategories, + products, + oneTimeTotal, + monthlyTotal, + oneTimeNet, + oneTimeTax, + oneTimeGross, + monthlyNet, + monthlyTax, + monthlyGross, + updatePriceModifier, + selectedEndCustomer, + customerData, + lastLicenseDate, + handleSubmit, + isSubmitting, + initialOrder, + prevStep, +}: StepSummaryProps) { + return ( + + +
+ +
+ Anfrage prüfen + + Fast fertig! Bitte überprüfen Sie Ihre Auswahl. + +
+ +
+ {finalItemsToShow.map((item, itemIdx) => ( +
+
+ Kasse: {item.deviceName} + {item.billingInterval === 'one_time' ? 'Kauf' : 'Abo'} +
+ + {visibleCategories.map(cat => { + const sel = item.selections[cat.id] + const selectedProds: Product[] = [] + if (cat.allow_multiselect && sel?.productIds) { + sel.productIds.forEach((pId: string) => { + const p = products.find(prod => prod.id === pId) + if (p) selectedProds.push(p) + }) + } else if (sel?.productId) { + const p = products.find(prod => prod.id === sel.productId) + if (p) selectedProds.push(p) + } + + if (selectedProds.length === 0) return null + + const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price) + const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0 + + return ( +
+
+ + {cat.name} +
+ {sortedProds.map((prod, idx) => { + const isFree = idx < freeLimit + const actualPrice = isFree ? 0 : prod.base_price + const catTotal = + Number(actualPrice) + + (sel?.moduleIds?.reduce((acc: number, mId: string) => { + const mod = prod.modules?.find(m => m.id === mId) + const qty = item.moduleQuantities[mId] || 1 + return acc + (Number(mod?.price ?? 0) * qty) + }, 0) || 0) + + return ( +
+
+ + {prod.name} {isFree && (Frei)} + + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(catTotal)} + +
+ {sel?.moduleIds && sel.moduleIds.length > 0 && ( +

+ Module: {sel.moduleIds + .map((id: string) => { + const mod = prod.modules?.find(m => m.id === id) + const qty = item.moduleQuantities[id] || 1 + return mod ? `${mod.name}${mod.has_quantity ? ` (x${qty})` : ''}` : '' + }) + .filter(Boolean) + .join(', ')} +

+ )} +
+ ) + })} +
+ ) + })} +
+ ))} + +
+ {selectedEndCustomer ? ( + <> +

+ + {selectedEndCustomer.company_name} +

+

{[selectedEndCustomer.first_name, selectedEndCustomer.last_name].filter(Boolean).join(' ')}

+

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

+

Endkunde Ihres Partners

+ {lastLicenseDate && ( +

+ + Letzte Lizenz vom: {new Date(lastLicenseDate).toLocaleDateString('de-DE')} +

+ )} + + ) : ( + <> +

{customerData.company_name}

+

{customerData.first_name} {customerData.last_name}

+

{customerData.address}, {customerData.zip_code} {customerData.city}

+ + )} +
+
+ {oneTimeTotal > 0 && monthlyTotal > 0 ? ( +
+
+

Einmaliger Betrag:

+
+ Netto: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)} +
+
+ zzgl. 19% MwSt.: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)} +
+
+ Brutto: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)} +
+
+ {updatePriceModifier.label && ( +
+ {updatePriceModifier.label} +
+ )} +
+

Monatlicher Betrag:

+
+ Netto: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl. +
+
+ zzgl. 19% MwSt.: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl. +
+
+ Brutto: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl. +
+
+
+ ) : oneTimeTotal > 0 ? ( +
+
+ Netto-Gesamtbetrag: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)} +
+
+ zzgl. 19% MwSt.: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)} +
+ {updatePriceModifier.label && ( +
+ {updatePriceModifier.label} +
+ )} +
+ Gesamtbetrag (brutto): + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)} +
+
+ ) : ( +
+
+ Netto-Gesamtbetrag: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl. +
+
+ zzgl. 19% MwSt.: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl. +
+
+ Gesamtbetrag (brutto): + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl. +
+
+ )} +

+ Mit dem Klick auf „Kostenpflichtig bestellen" akzeptieren Sie unsere AGB und die{' '} + + Datenschutzerklärung + . +

+
+ + + + +
+ ) +} diff --git a/shop/components/wizard/summary-sidebar.tsx b/shop/components/wizard/summary-sidebar.tsx new file mode 100644 index 0000000..3ea8159 --- /dev/null +++ b/shop/components/wizard/summary-sidebar.tsx @@ -0,0 +1,299 @@ +'use client' + +import React from 'react' +import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/card' +import { Separator } from '@/components/ui/separator' +import { Label } from '@/components/ui/label' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { Pencil, Trash2, UserPlus, ChevronRight, AlertCircle, Check } from 'lucide-react' +import { Category, Product, CategorySelection } from '@/lib/types' + +interface SummarySidebarProps { + visibleCategories: Category[] + selections: Record + products: Product[] + moduleQuantities: Record + billingLabel: (interval: string) => string + oneTimeTotal: number + monthlyTotal: number + oneTimeNet: number + oneTimeTax: number + oneTimeGross: number + monthlyNet: number + monthlyTax: number + monthlyGross: number + updatePriceModifier: { label?: string } + allCategoriesFilled: boolean + productValidationErrors: string[] + basketItems: any[] + editingIdx: number | null + editBasketItem: (idx: number) => void + deleteBasketItem: (idx: number) => void + deviceName: string + setDeviceName: (name: string) => void + addToBasket: () => void + isNextStepDisabled: boolean + hasActiveSelection: boolean + nextStep: () => void + prevStep: () => void +} + +export function SummarySidebar({ + visibleCategories, + selections, + products, + moduleQuantities, + billingLabel, + oneTimeTotal, + monthlyTotal, + oneTimeNet, + oneTimeTax, + oneTimeGross, + monthlyNet, + monthlyTax, + monthlyGross, + updatePriceModifier, + allCategoriesFilled, + productValidationErrors, + basketItems, + editingIdx, + editBasketItem, + deleteBasketItem, + deviceName, + setDeviceName, + addToBasket, + isNextStepDisabled, + hasActiveSelection, + nextStep, + prevStep, +}: SummarySidebarProps) { + return ( +
+ + + Zusammenfassung + + + {visibleCategories.map(cat => { + const sel = selections[cat.id] + const selectedProds: Product[] = [] + if (cat.allow_multiselect && sel?.productIds) { + sel.productIds.forEach(pId => { + const p = products.find(prod => prod.id === pId) + if (p) selectedProds.push(p) + }) + } else if (sel?.productId) { + const p = products.find(prod => prod.id === sel.productId) + if (p) selectedProds.push(p) + } + + if (selectedProds.length === 0) return ( +
+ {cat.is_required ? ( + + ) : ( + + )} + {cat.name}: {cat.is_required ? 'nicht gewählt' : 'nicht gewählt (optional)'} +
+ ) + + const sortedProds = [...selectedProds].sort((a, b) => a.base_price - b.base_price) + const freeLimit = cat.allow_multiselect ? cat.free_items_limit : 0 + + return ( +
+
+ {cat.name} +
+ {sortedProds.map((prod, idx) => { + const isFree = idx < freeLimit + const actualPrice = isFree ? 0 : prod.base_price + return ( +
+
+ {prod.name} {isFree && (Frei)} + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(actualPrice)} + {' '}{billingLabel(prod.billing_interval)} + +
+ {sel.moduleIds.map(mId => { + const mod = prod.modules?.find(m => m.id === mId) + const qty = moduleQuantities[mId] || 1 + return mod ? ( +
+ + {mod.name} {mod.has_quantity ? `(x${qty})` : ''} + + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price * qty)} + +
+ ) : null + })} +
+ ) + })} +
+ ) + })} + + {oneTimeTotal > 0 && monthlyTotal > 0 ? ( +
+
+

Einmalig:

+
+ Netto: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)} +
+
+ zzgl. 19% MwSt.: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)} +
+
+ Gesamt (brutto): + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)} +
+
+
+

Monatlich:

+
+ Netto: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl. +
+
+ zzgl. 19% MwSt.: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl. +
+
+ Gesamt (brutto): + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl. +
+
+
+ ) : oneTimeTotal > 0 ? ( +
+
+ Netto-Gesamtbetrag: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeNet)} +
+
+ zzgl. 19% MwSt.: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTax)} +
+
+ Gesamtbetrag (brutto): + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeGross)} +
+
+ ) : ( +
+
+ Netto-Gesamtbetrag: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyNet)} / mtl. +
+
+ zzgl. 19% MwSt.: + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyTax)} / mtl. +
+
+ Gesamtbetrag (brutto): + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(monthlyGross)} / mtl. +
+
+ )} + {updatePriceModifier.label && ( +
+

+ + Update-Rabatt aktiv +

+

{updatePriceModifier.label}

+
+ )} + {!allCategoriesFilled && ( +

+ + Bitte aus jeder Pflichtkategorie einen Artikel wählen. +

+ )} + {productValidationErrors.map((err, errIdx) => ( +

+ + {err} +

+ ))} + {basketItems.length > 0 && ( +
+

Warenkorb ({basketItems.length}):

+
+ {basketItems.map((item, idx) => ( +
+
+ {item.deviceName} + {item.billingInterval === 'one_time' ? 'Kauf' : 'Abo'} +
+
+ + +
+
+ ))} +
+
+ )} +
+ +
+ + setDeviceName(e.target.value)} + className="bg-white/5 border-white/10 text-white text-xs h-9 rounded-lg" + /> +
+ + + + +
+
+
+ ) +} diff --git a/shop/components/wizard/toast-notification.tsx b/shop/components/wizard/toast-notification.tsx new file mode 100644 index 0000000..3c6a730 --- /dev/null +++ b/shop/components/wizard/toast-notification.tsx @@ -0,0 +1,40 @@ +'use client' + +import { motion, AnimatePresence } from 'framer-motion' +import { AlertCircle } from 'lucide-react' +import { Button } from '@/components/ui/button' + +interface ToastProps { + toast: { message: string; type: 'error' | 'success' } | null + onClose: () => void +} + +export function ToastNotification({ toast, onClose }: ToastProps) { + return ( + + {toast && ( + + +

{toast.message}

+ +
+ )} +
+ ) +} diff --git a/shop/package-lock.json b/shop/package-lock.json index 7562e61..24c0158 100644 --- a/shop/package-lock.json +++ b/shop/package-lock.json @@ -3881,6 +3881,7 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.105.1.tgz", "integrity": "sha512-4gn6HmsAkCCVU7p8JmgKGhHJ5Btod4ZzSp8qKZf4JHaTxbhaIK86/usHzeLxWv7EJJDhBmILDmJOSOf9iF4CLA==", "license": "MIT", + "peer": true, "dependencies": { "@supabase/auth-js": "2.105.1", "@supabase/functions-js": "2.105.1", @@ -3979,6 +3980,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3989,6 +3991,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4047,6 +4050,7 @@ "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/types": "8.59.1", @@ -4668,6 +4672,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5180,6 +5185,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -6004,6 +6010,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6177,6 +6184,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7549,6 +7557,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -8444,6 +8453,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", @@ -8624,6 +8634,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9069,6 +9080,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9078,6 +9090,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9090,6 +9103,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.74.0.tgz", "integrity": "sha512-yR6wHr99p9wFv686jhRWVSFhUvDvNbdUf2dKlbno8/VKOCuoNobDGC6S+M2dua9A9Yo8vpcrp8assIYbsZCQ9g==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -10185,6 +10199,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10371,6 +10386,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver"