diff --git a/shop/components/admin/create-product-dialog.tsx b/shop/components/admin/create-product-dialog.tsx index 9050252..55ad437 100644 --- a/shop/components/admin/create-product-dialog.tsx +++ b/shop/components/admin/create-product-dialog.tsx @@ -58,7 +58,6 @@ const productSchema = z.object({ billing_interval: z.enum(['one_time', 'monthly']).default('monthly'), show_in_kauf: z.boolean().default(true), show_in_abo: z.boolean().default(true), - has_quantity: z.boolean().default(false), requirements: z.array(z.string()).default([]), exclusions: z.array(z.string()).default([]), modules: z.array(moduleSchema).default([]), @@ -92,7 +91,6 @@ export function CreateProductDialog({ billing_interval: product?.billing_interval || 'monthly', show_in_kauf: product ? (product.show_in_kauf ?? true) : true, show_in_abo: product ? (product.show_in_abo ?? true) : true, - has_quantity: product ? (product.has_quantity ?? false) : false, requirements: product?.requirements || [], exclusions: product?.exclusions || [], modules: product?.modules?.map(m => ({ @@ -240,7 +238,7 @@ export function CreateProductDialog({ )} /> -
+
- In Kauf anzeigen - Produkt im Kauf anzeigen + In Kauf-Auswahl anzeigen + Produkt im Kauf-Zweig anzeigen
)} @@ -271,26 +269,8 @@ export function CreateProductDialog({ />
- In Abo anzeigen - Produkt im Abo anzeigen -
- - )} - /> - ( - - - - -
- Mengeneingabe - Mengeneingabe erlauben + In Abo-Auswahl anzeigen + Produkt im Abo-Zweig anzeigen
)} diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index 9774ae9..b363a25 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -141,7 +141,7 @@ export function OrderWizard({ const [customerMode, setCustomerMode] = useState<'select' | 'create'>('select') const [isCreatingCustomer, setIsCreatingCustomer] = useState(false) const [newCustomerForm, setNewCustomerForm] = useState({ - company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '', email: '', + company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '', bank_iban: '', bank_bic: '', bank_name: '', bank_owner: '', }) @@ -162,16 +162,6 @@ export function OrderWizard({ return quantities }) - // Produktmengen (productId -> Menge) - const [productQuantities, setProductQuantities] = useState>(() => { - if (!initialOrder) return {} - const quantities: Record = {} - initialOrder.order_data?.items?.forEach(item => { - quantities[item.product_id] = item.quantity ?? 1 - }) - return quantities - }) - // Datum der letzten Lizenz (Bestandskunden-Update-Logik) const [lastLicenseDate, setLastLicenseDate] = useState( initialOrder?.order_data?.last_license_date ?? "" @@ -338,7 +328,6 @@ export function OrderWizard({ sortedProds.forEach((prod, sortedIndex) => { const base = sortedIndex < freeLimit ? 0 : Number(prod.base_price) - const productQty = productQuantities[prod.id] || 1 let modulesSum = 0 sel.moduleIds.forEach(mId => { const mod = prod.modules?.find(m => m.id === mId) @@ -347,7 +336,7 @@ export function OrderWizard({ modulesSum += Number(mod.price) * qty } }) - const totalItem = (base + modulesSum) * productQty + const totalItem = base + modulesSum if (prod.billing_interval === 'one_time') { oneTime += totalItem } else { @@ -475,14 +464,8 @@ export function OrderWizard({ const handleBillingIntervalChange = (val: 'one_time' | 'monthly') => { setSelectedBillingInterval(val) setModuleQuantities({}) - setProductQuantities({}) if (val === 'monthly') { setLastLicenseDate("") - if (!selectedEndCustomerId && endCustomers.length > 0) { - setSelectedEndCustomerId(endCustomers[0].id) - } - } else { - setSelectedEndCustomerId(null) } const newSelections: Record = {} categories.forEach(cat => { @@ -514,7 +497,7 @@ export function OrderWizard({ setSelectedEndCustomerId(created.id) setCustomerMode('select') setNewCustomerForm({ - company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '', email: '', + company_name: '', vat_id: '', first_name: '', last_name: '', street: '', zip: '', city: '', bank_iban: '', bank_bic: '', bank_name: '', bank_owner: '', }) } catch (e: any) { @@ -533,7 +516,6 @@ export function OrderWizard({ order = await updateOrder(initialOrder.id, { selections, moduleQuantities, - productQuantities, customerProfile: customerData, endCustomerId: selectedEndCustomerId, endCustomer: selectedEndCustomer, @@ -545,7 +527,6 @@ export function OrderWizard({ order = await submitOrder({ selections, moduleQuantities, - productQuantities, products, categories, customerProfile: customerData, @@ -581,7 +562,7 @@ export function OrderWizard({ {step > s ? : s}
= s ? 'text-primary' : 'text-slate-500'}`}> - {s === 1 ? 'Modell' : s === 2 ? 'Kunde' : s === 3 ? 'Software' : 'Abschluss'} + {s === 1 ? 'Kunde' : s === 2 ? 'Modell' : s === 3 ? 'Software' : 'Abschluss'}
))} @@ -589,7 +570,7 @@ export function OrderWizard({ - {/* ───── Schritt 1: Abrechnungsmodell wählen ───── */} + {/* ───── Schritt 1: Endkunde wählen / anlegen ───── */} {step === 1 && ( + + + + + Für wen bestellen Sie? + + + Wählen Sie einen Endkunden aus Ihrem CRM 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 && ( + @@ -662,234 +827,22 @@ export function OrderWizard({ )} - - - - - - - )} - - {/* ───── Schritt 2: Stammdaten / Endkunde wählen ───── */} - {step === 2 && ( - - - - - - {selectedBillingInterval === 'one_time' ? 'Ihre Rechnungsdaten (Stammdaten)' : 'Für wen bestellen Sie?'} - - - {selectedBillingInterval === 'one_time' - ? 'Bestätigen Sie Ihre Firmen-Stammdaten für den Kauf.' - : 'Wählen Sie einen Endkunden aus Ihrem CRM oder legen Sie einen neuen an.'} - - - - {selectedBillingInterval === 'one_time' ? ( -
-
-

Rechnungsempfänger Details

-
-
-

Unternehmen

-

{customerData.company_name || 'Nicht angegeben'}

-
-
-

USt-IdNr.

-

{customerData.vat_id || 'Nicht angegeben'}

-
-
-

Ansprechpartner

-

{customerData.first_name} {customerData.last_name}

-
-
-

Adresse

-

- {customerData.address}
- {customerData.zip_code} {customerData.city} -

-
-
-
- -
- - setLastLicenseDate(e.target.value)} - className="bg-[#0b1329] border-white/10 text-white focus:border-primary" - /> -

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

-
-
- ) : ( -
- {/* 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: 'E-Mail Adresse', key: 'email', span: true, placeholder: 'kunden@firma.de' }, - { 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" - /> -
- ))} -
- - -
-
- )} + {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. +

)} @@ -897,16 +850,8 @@ export function OrderWizard({ - @@ -1048,29 +993,6 @@ export function OrderWizard({ )} - {isChecked && product.has_quantity && ( -
- - { - const val = Math.max(1, parseInt(e.target.value) || 1) - setProductQuantities(prev => ({ ...prev, [product.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(product.base_price * (productQuantities[product.id] || 1))} - -
- )}
) })} @@ -1081,69 +1003,43 @@ export function OrderWizard({ onValueChange={id => selectProduct(cat.id, id)} className="grid gap-3" > - {catProducts.map(product => { - const isChecked = sel?.productId === product.id - return ( -
- - - {isChecked && product.has_quantity && ( -
- - { - const val = Math.max(1, parseInt(e.target.value) || 1) - setProductQuantities(prev => ({ ...prev, [product.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(product.base_price * (productQuantities[product.id] || 1))} + {catProducts.map(product => ( +
+ +
- ) - })} + {product.modules && product.modules.length > 0 && ( + + {product.modules.length} optionale Module verfügbar + + )} + +
+ ))} )} @@ -1273,14 +1169,12 @@ export function OrderWizard({ {sortedProds.map((prod, idx) => { const isFree = idx < freeLimit const actualPrice = isFree ? 0 : prod.base_price - const prodQty = productQuantities[prod.id] || 1 - const qtyLabel = prod.has_quantity ? `(x${prodQty})` : '' return (
- {prod.name} {qtyLabel} {isFree && (Frei)} + {prod.name} {isFree && (Frei)} - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(actualPrice * prodQty)} + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(actualPrice)} {' '}{billingLabel(prod.billing_interval)}
@@ -1289,9 +1183,9 @@ export function OrderWizard({ const qty = moduleQuantities[mId] || 1 return mod ? (
- + {mod.name} {mod.has_quantity ? `(x${qty})` : ''} {prod.has_quantity ? `(x${prodQty} Art.)` : ''} + + {mod.name} {mod.has_quantity ? `(x${qty})` : ''} - {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price * qty * prodQty)} + {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price * qty)}
) : null @@ -1456,21 +1350,19 @@ export function OrderWizard({ {sortedProds.map((prod, idx) => { const isFree = idx < freeLimit const actualPrice = isFree ? 0 : prod.base_price - const prodQty = productQuantities[prod.id] || 1 - const baseTotal = + const catTotal = Number(actualPrice) + (sel?.moduleIds?.reduce((acc, mId) => { const mod = prod.modules?.find(m => m.id === mId) const qty = moduleQuantities[mId] || 1 return acc + (Number(mod?.price ?? 0) * qty) }, 0) || 0) - const catTotal = baseTotal * prodQty return (
- {prod.name} {prod.has_quantity ? `(x${prodQty})` : ''} {isFree && (Frei)} + {prod.name} {isFree && (Frei)} {new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(catTotal)} @@ -1483,7 +1375,7 @@ export function OrderWizard({ .map(id => { const mod = prod.modules?.find(m => m.id === id) const qty = moduleQuantities[id] || 1 - return mod ? `${mod.name}${mod.has_quantity ? ` (x${qty})` : ''} ${prod.has_quantity ? `(x${prodQty} Art.)` : ''}` : '' + return mod ? `${mod.name}${mod.has_quantity ? ` (x${qty})` : ''}` : '' }) .filter(Boolean) .join(', ')} diff --git a/shop/lib/actions/orders.ts b/shop/lib/actions/orders.ts index 7bf1697..b06b78a 100644 --- a/shop/lib/actions/orders.ts +++ b/shop/lib/actions/orders.ts @@ -49,7 +49,6 @@ function hashOrderSnapshot(snapshot: object): string { export async function submitOrder(params: { selections: WizardSelections moduleQuantities?: Record - productQuantities?: Record products?: Product[] categories?: Category[] customerProfile: Partial @@ -58,7 +57,7 @@ export async function submitOrder(params: { billingInterval?: 'one_time' | 'monthly' lastLicenseDate?: string | null }): Promise { - const { selections, moduleQuantities, productQuantities, customerProfile, endCustomerId, endCustomer, billingInterval, lastLicenseDate } = params + const { selections, moduleQuantities, customerProfile, endCustomerId, endCustomer, billingInterval, lastLicenseDate } = params const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() @@ -86,7 +85,7 @@ export async function submitOrder(params: { // 1. Snapshots aufbauen // Wenn Endkunde vorhanden: dessen Daten einfrieren; sonst Partner-Profil (Fallback) const customerSnapshot = buildCustomerSnapshot(customerProfile, endCustomer ?? null) - const orderSnapshot = buildOrderSnapshot(selections, dbProducts, dbCategories, billingInterval, moduleQuantities, lastLicenseDate, productQuantities) + const orderSnapshot = buildOrderSnapshot(selections, dbProducts, dbCategories, billingInterval, moduleQuantities, lastLicenseDate) // 2. Idempotenz-Guard: Prüfen ob identische Bestellung in den letzten 5 Minuten existiert const orderHash = hashOrderSnapshot(orderSnapshot) @@ -367,7 +366,6 @@ export async function updateOrder( params: { selections: WizardSelections moduleQuantities?: Record - productQuantities?: Record customerProfile: Partial endCustomerId?: string | null endCustomer?: EndCustomer | null @@ -376,7 +374,7 @@ export async function updateOrder( companyId?: string | null } ): Promise { - const { selections, moduleQuantities, productQuantities, customerProfile, endCustomerId, endCustomer, billingInterval, lastLicenseDate, companyId } = params + const { selections, moduleQuantities, customerProfile, endCustomerId, endCustomer, billingInterval, lastLicenseDate, companyId } = params const supabase = await createClient() const admin = createAdminClient() @@ -436,7 +434,7 @@ export async function updateOrder( // 1. Snapshots aufbauen const customerSnapshot = buildCustomerSnapshot(customerProfile, endCustomer ?? null) - const orderSnapshot = buildOrderSnapshot(selections, dbProducts, dbCategories, billingInterval, moduleQuantities, lastLicenseDate, productQuantities) + const orderSnapshot = buildOrderSnapshot(selections, dbProducts, dbCategories, billingInterval, moduleQuantities, lastLicenseDate) const orderHash = hashOrderSnapshot(orderSnapshot) // 2. Bestellung in DB aktualisieren diff --git a/shop/lib/license-transform.ts b/shop/lib/license-transform.ts index 8b91887..c2fd4cb 100644 --- a/shop/lib/license-transform.ts +++ b/shop/lib/license-transform.ts @@ -91,8 +91,7 @@ export function buildOrderSnapshot( categories: Category[], billingInterval?: 'one_time' | 'monthly', moduleQuantities?: Record, - lastLicenseDate?: string | null, - productQuantities?: Record + lastLicenseDate?: string | null ): OrderSnapshot { const items: OrderItem[] = [] let subtotal = 0 @@ -136,12 +135,11 @@ export function buildOrderSnapshot( } }) - const productQty = productQuantities?.[prod.id] || 1 const moduleTotal = selectedModules.reduce((acc, m) => acc + (m.total_price || m.price), 0) // If this product falls under the free limit, set its base price to 0 const actualBasePrice = (sortedIndex < freeLimit) ? 0 : prod.base_price - const itemTotal = (actualBasePrice + moduleTotal) * productQty + const itemTotal = actualBasePrice + moduleTotal subtotal += itemTotal items.push({ @@ -151,7 +149,6 @@ export function buildOrderSnapshot( product_name: prod.name + (actualBasePrice === 0 ? " (Inklusive/Frei)" : ""), base_price: actualBasePrice, billing_interval: prod.billing_interval, - quantity: productQty, selected_modules: selectedModules, item_total: itemTotal, }) diff --git a/shop/lib/types.ts b/shop/lib/types.ts index db0dc4a..18ba045 100644 --- a/shop/lib/types.ts +++ b/shop/lib/types.ts @@ -17,7 +17,6 @@ export type Product = { show_in_abo?: boolean requirements?: string[] exclusions?: string[] - has_quantity?: boolean } export type Category = { @@ -132,7 +131,6 @@ export type OrderItem = { product_name: string base_price: number billing_interval: BillingInterval - quantity?: number selected_modules: OrderModuleSnapshot[] item_total: number } diff --git a/shop/supabase/migrations/20260709145900_add_has_quantity_to_products.sql b/shop/supabase/migrations/20260709145900_add_has_quantity_to_products.sql deleted file mode 100644 index 4076870..0000000 --- a/shop/supabase/migrations/20260709145900_add_has_quantity_to_products.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Add has_quantity column to products table -ALTER TABLE public.products ADD COLUMN IF NOT EXISTS has_quantity BOOLEAN DEFAULT false NOT NULL;