- {/* Admin Company Selector */}
- {isAdmin && (
-
-
-
- Zugeordnetes Unternehmen (Admin-Option)
-
- setSelectedCompanyId(e.target.value || null)}
- className="flex h-9 w-full rounded-md border border-white/10 bg-slate-900 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary text-white"
- >
- Keine Firma zugewiesen
- {companies.map((c: any) => (
-
- {c.name}
-
- ))}
-
-
- )}
-
- {/* Modus-Toggle */}
-
- setCustomerMode('select')}
- className={`gap-2 ${customerMode === 'select' ? '' : 'text-white'}`}
- >
- Bestandskunde wählen
-
- setCustomerMode('create')}
- className={`gap-2 ${customerMode === 'create' ? '' : 'text-white'}`}
- >
- Neuen Kunden anlegen
-
-
-
- {/* Modus A: Bestandskunde wählen */}
- {customerMode === 'select' && (
-
- {endCustomers.filter(c => !c.is_anonymized).length === 0 ? (
-
-
-
Noch keine Endkunden angelegt.
-
setCustomerMode('create')} className="border-white/10 text-white">
- Ersten Kunden anlegen
-
-
- ) : (
-
-
-
- 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 => (
-
- setSelectedEndCustomerId(customer.id)}
- className="sr-only"
- />
-
-
{customer.company_name}
-
- {[customer.first_name, customer.last_name].filter(Boolean).join(' ')}
- {customer.first_name || customer.last_name ? ' · ' : ''}
- {[customer.street, customer.zip, customer.city].filter(Boolean).join(', ')}
-
-
- {selectedEndCustomerId === customer.id && (
-
- )}
-
- ))}
-
- )}
-
- )}
-
- )}
-
- {/* 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 }) => (
-
- {label}
- setNewCustomerForm(prev => ({ ...prev, [key]: e.target.value }))}
- placeholder={placeholder}
- className="bg-white/5 border-white/10 text-white placeholder:text-slate-500"
- />
-
- ))}
-
-
- {isCreatingCustomer ? : }
- Kunden speichern & auswählen
-
- setCustomerMode('select')}>
- Abbrechen
-
-
-
- )}
+ {selectedBillingInterval === 'one_time' && customerMode === 'select' && selectedEndCustomerId && (
+
+
+
+ Datum der letzten CAS-Lizenz (falls vorhanden)
+
+
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({
Zurück
-
!c.is_anonymized).length > 0 && !selectedEndCustomerId)
- )
- }
- >
- Weiter zur Software-Auswahl
+
+ Weiter zur Software
@@ -1048,29 +993,6 @@ export function OrderWizard({
)}
- {isChecked && product.has_quantity && (
-
- Menge:
- {
- 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 (
-
-
-
-
-
- {product.name}
-
- {product.billing_interval === 'one_time' ? 'Einmalig' : 'Abo/Monat'}
-
-
-
- {new Intl.NumberFormat('de-DE', {
- style: 'currency',
- currency: 'EUR',
- }).format(product.base_price)}{' '}
- {billingLabel(product.billing_interval)}
-
-
- {product.description && (
- {product.description}
- )}
- {product.modules && product.modules.length > 0 && (
-
- {product.modules.length} optionale Module verfügbar
-
- )}
-
- {isChecked && product.has_quantity && (
-
-
Menge:
-
{
- 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.name}
+
+ {product.billing_interval === 'one_time' ? 'Einmalig' : 'Abo/Monat'}
+
+ {new Intl.NumberFormat('de-DE', {
+ style: 'currency',
+ currency: 'EUR',
+ }).format(product.base_price)}{' '}
+ {billingLabel(product.billing_interval)}
+
+
+ {product.description && (
+ {product.description}
)}
-
- )
- })}
+ {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;