feat: Add quantity support for product modules, multiply prices in order snapshot and pdf
Some checks failed
Staging Build / build (push) Has been cancelled
Some checks failed
Staging Build / build (push) Has been cancelled
This commit is contained in:
@@ -45,6 +45,7 @@ const moduleSchema = z.object({
|
|||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
price: z.coerce.number().min(0, 'Preis darf nicht negativ sein'),
|
price: z.coerce.number().min(0, 'Preis darf nicht negativ sein'),
|
||||||
is_required: z.boolean().default(false),
|
is_required: z.boolean().default(false),
|
||||||
|
has_quantity: z.boolean().default(false),
|
||||||
requirements: z.array(z.string()).default([]),
|
requirements: z.array(z.string()).default([]),
|
||||||
exclusions: z.array(z.string()).default([]),
|
exclusions: z.array(z.string()).default([]),
|
||||||
})
|
})
|
||||||
@@ -82,6 +83,7 @@ export function CreateProductDialog({ children, categories, product }: { childre
|
|||||||
description: m.description || '',
|
description: m.description || '',
|
||||||
price: m.price,
|
price: m.price,
|
||||||
is_required: false,
|
is_required: false,
|
||||||
|
has_quantity: m.has_quantity ?? false,
|
||||||
requirements: m.requirements || [],
|
requirements: m.requirements || [],
|
||||||
exclusions: m.exclusions || [],
|
exclusions: m.exclusions || [],
|
||||||
})) || [],
|
})) || [],
|
||||||
@@ -291,6 +293,7 @@ export function CreateProductDialog({ children, categories, product }: { childre
|
|||||||
name: '',
|
name: '',
|
||||||
price: 0,
|
price: 0,
|
||||||
is_required: false,
|
is_required: false,
|
||||||
|
has_quantity: false,
|
||||||
description: '',
|
description: '',
|
||||||
requirements: [],
|
requirements: [],
|
||||||
exclusions: []
|
exclusions: []
|
||||||
@@ -442,21 +445,35 @@ export function CreateProductDialog({ children, categories, product }: { childre
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center gap-6">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`modules.${index}.is_required`}
|
name={`modules.${index}.is_required`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<div className="space-y-1 leading-none">
|
<FormLabel className="text-slate-900 dark:text-white font-medium cursor-pointer text-xs">Erforderlich</FormLabel>
|
||||||
<FormLabel className="text-slate-900 dark:text-white font-medium cursor-pointer">Erforderlich</FormLabel>
|
</FormItem>
|
||||||
</div>
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`modules.${index}.has_quantity`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="text-slate-900 dark:text-white font-medium cursor-pointer text-xs">Mengeneingabe erlauben</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -153,18 +153,25 @@ export const InvoicePDF = ({ order, orderSnapshot, customer }: any) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{item.selected_modules?.map((mod: any, mIdx: number) => (
|
{item.selected_modules?.map((mod: any, mIdx: number) => {
|
||||||
<View key={mIdx} style={styles.tableRow}>
|
const qty = mod.quantity || 1;
|
||||||
<View style={[styles.tableCol, { paddingLeft: 20 }]}>
|
const price = mod.total_price ?? (mod.price * qty);
|
||||||
<Text style={{ color: '#666' }}>+ {mod.module_name}</Text>
|
const hasQty = qty > 1;
|
||||||
|
return (
|
||||||
|
<View key={mIdx} style={styles.tableRow}>
|
||||||
|
<View style={[styles.tableCol, { paddingLeft: 20 }]}>
|
||||||
|
<Text style={{ color: '#666' }}>
|
||||||
|
+ {mod.module_name} {hasQty ? `(x${qty})` : ''}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.tableColPrice}>
|
||||||
|
<Text style={{ color: '#666' }}>
|
||||||
|
+{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(price)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.tableColPrice}>
|
);
|
||||||
<Text style={{ color: '#666' }}>
|
})}
|
||||||
+{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ export function OrderWizard({
|
|||||||
// Abrechnungsmodell-Auswahl ('one_time' = Kauf, 'monthly' = Abo)
|
// Abrechnungsmodell-Auswahl ('one_time' = Kauf, 'monthly' = Abo)
|
||||||
const [selectedBillingInterval, setSelectedBillingInterval] = useState<'one_time' | 'monthly'>('monthly')
|
const [selectedBillingInterval, setSelectedBillingInterval] = useState<'one_time' | 'monthly'>('monthly')
|
||||||
|
|
||||||
|
// Modulmengen (moduleId -> Menge)
|
||||||
|
const [moduleQuantities, setModuleQuantities] = useState<Record<string, number>>({})
|
||||||
|
|
||||||
// Per-category selection map: categoryId -> { productId, moduleIds }
|
// Per-category selection map: categoryId -> { productId, moduleIds }
|
||||||
const [selections, setSelections] = useState<Record<string, CategorySelection>>(() => {
|
const [selections, setSelections] = useState<Record<string, CategorySelection>>(() => {
|
||||||
const init: Record<string, CategorySelection> = {}
|
const init: Record<string, CategorySelection> = {}
|
||||||
@@ -149,7 +152,10 @@ export function OrderWizard({
|
|||||||
let modulesSum = 0
|
let modulesSum = 0
|
||||||
sel.moduleIds.forEach(mId => {
|
sel.moduleIds.forEach(mId => {
|
||||||
const mod = prod.modules?.find(m => m.id === mId)
|
const mod = prod.modules?.find(m => m.id === mId)
|
||||||
if (mod) modulesSum += Number(mod.price)
|
if (mod) {
|
||||||
|
const qty = moduleQuantities[mId] || 1
|
||||||
|
modulesSum += Number(mod.price) * qty
|
||||||
|
}
|
||||||
})
|
})
|
||||||
const totalItem = base + modulesSum
|
const totalItem = base + modulesSum
|
||||||
if (prod.billing_interval === 'one_time') {
|
if (prod.billing_interval === 'one_time') {
|
||||||
@@ -159,7 +165,7 @@ export function OrderWizard({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { monthlyTotal: monthly, oneTimeTotal: oneTime }
|
return { monthlyTotal: monthly, oneTimeTotal: oneTime }
|
||||||
}, [selections, products, categories])
|
}, [selections, products, categories, moduleQuantities])
|
||||||
|
|
||||||
// Helper: update product selection for a category (resets modules)
|
// Helper: update product selection for a category (resets modules)
|
||||||
function selectProduct(catId: string, productId: string) {
|
function selectProduct(catId: string, productId: string) {
|
||||||
@@ -178,6 +184,13 @@ export function OrderWizard({
|
|||||||
const newModuleIds = toggleModuleInList(prod?.modules ?? [], sel.moduleIds, moduleId)
|
const newModuleIds = toggleModuleInList(prod?.modules ?? [], sel.moduleIds, moduleId)
|
||||||
return { ...prev, [catId]: { ...sel, moduleIds: newModuleIds } }
|
return { ...prev, [catId]: { ...sel, moduleIds: newModuleIds } }
|
||||||
})
|
})
|
||||||
|
setModuleQuantities(prev => {
|
||||||
|
if (prev[moduleId] !== undefined) {
|
||||||
|
const { [moduleId]: _, ...rest } = prev
|
||||||
|
return rest
|
||||||
|
}
|
||||||
|
return { ...prev, [moduleId]: 1 }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextStep = () => setStep(s => s + 1)
|
const nextStep = () => setStep(s => s + 1)
|
||||||
@@ -186,6 +199,7 @@ export function OrderWizard({
|
|||||||
// Abrechnungsmodell wechseln (und Selektionen der Kategorien anpassen)
|
// Abrechnungsmodell wechseln (und Selektionen der Kategorien anpassen)
|
||||||
const handleBillingIntervalChange = (val: 'one_time' | 'monthly') => {
|
const handleBillingIntervalChange = (val: 'one_time' | 'monthly') => {
|
||||||
setSelectedBillingInterval(val)
|
setSelectedBillingInterval(val)
|
||||||
|
setModuleQuantities({})
|
||||||
const newSelections: Record<string, CategorySelection> = {}
|
const newSelections: Record<string, CategorySelection> = {}
|
||||||
categories.forEach(cat => {
|
categories.forEach(cat => {
|
||||||
const matchingProd = products.find(p =>
|
const matchingProd = products.find(p =>
|
||||||
@@ -220,6 +234,7 @@ export function OrderWizard({
|
|||||||
try {
|
try {
|
||||||
const order = await submitOrder({
|
const order = await submitOrder({
|
||||||
selections,
|
selections,
|
||||||
|
moduleQuantities,
|
||||||
products,
|
products,
|
||||||
categories,
|
categories,
|
||||||
customerProfile: customerData,
|
customerProfile: customerData,
|
||||||
@@ -612,40 +627,67 @@ export function OrderWizard({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={module.id}
|
key={module.id}
|
||||||
className={`flex items-start space-x-3 p-3 rounded-lg border border-white/5 bg-white/5 ml-2 transition-colors ${disabled ? 'opacity-50' : 'hover:bg-white/10'}`}
|
className={`flex flex-col p-3 rounded-lg border border-white/5 bg-white/5 ml-2 transition-colors ${disabled ? 'opacity-50' : 'hover:bg-white/10'}`}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<div className="flex items-start space-x-3">
|
||||||
id={`mod-${cat.id}-${module.id}`}
|
<Checkbox
|
||||||
checked={checked}
|
id={`mod-${cat.id}-${module.id}`}
|
||||||
onCheckedChange={() => toggleModule(cat.id, module.id)}
|
checked={checked}
|
||||||
disabled={disabled}
|
onCheckedChange={() => toggleModule(cat.id, module.id)}
|
||||||
/>
|
disabled={disabled}
|
||||||
<div className="flex-1">
|
/>
|
||||||
<Label
|
<div className="flex-1">
|
||||||
htmlFor={`mod-${cat.id}-${module.id}`}
|
<Label
|
||||||
className={`font-medium cursor-pointer flex justify-between text-white ${disabled ? 'cursor-not-allowed' : ''}`}
|
htmlFor={`mod-${cat.id}-${module.id}`}
|
||||||
>
|
className={`font-medium cursor-pointer flex justify-between text-white ${disabled ? 'cursor-not-allowed' : ''}`}
|
||||||
<span>{module.name}</span>
|
>
|
||||||
<span className="text-primary font-bold">
|
<span>{module.name}</span>
|
||||||
+{new Intl.NumberFormat('de-DE', {
|
<span className="text-primary font-bold">
|
||||||
|
+{new Intl.NumberFormat('de-DE', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'EUR',
|
||||||
|
}).format(module.price)}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
{module.description && (
|
||||||
|
<p className="text-xs text-slate-400">{module.description}</p>
|
||||||
|
)}
|
||||||
|
{disabled && (
|
||||||
|
<p className="text-[10px] text-destructive mt-1">
|
||||||
|
{module.requirements?.some(
|
||||||
|
reqId => !sel?.moduleIds.includes(reqId)
|
||||||
|
)
|
||||||
|
? 'Benötigt weitere Module'
|
||||||
|
: 'Nicht kombinierbar mit aktueller Auswahl'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scalable Quantity Input */}
|
||||||
|
{checked && module.has_quantity && (
|
||||||
|
<div className="flex items-center gap-3 mt-3 pl-8 pt-2 border-t border-white/5">
|
||||||
|
<Label htmlFor={`qty-${module.id}`} className="text-xs text-slate-400">Menge:</Label>
|
||||||
|
<Input
|
||||||
|
id={`qty-${module.id}`}
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={999}
|
||||||
|
value={moduleQuantities[module.id] || 1}
|
||||||
|
onChange={(e) => {
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-slate-500">
|
||||||
|
Gesamt: {new Intl.NumberFormat('de-DE', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'EUR',
|
currency: 'EUR',
|
||||||
}).format(module.price)}
|
}).format(module.price * (moduleQuantities[module.id] || 1))}
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</div>
|
||||||
{module.description && (
|
)}
|
||||||
<p className="text-xs text-slate-400">{module.description}</p>
|
|
||||||
)}
|
|
||||||
{disabled && (
|
|
||||||
<p className="text-[10px] text-destructive mt-1">
|
|
||||||
{module.requirements?.some(
|
|
||||||
reqId => !sel?.moduleIds.includes(reqId)
|
|
||||||
)
|
|
||||||
? 'Benötigt weitere Module'
|
|
||||||
: 'Nicht kombinierbar mit aktueller Auswahl'}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -692,11 +734,12 @@ export function OrderWizard({
|
|||||||
</div>
|
</div>
|
||||||
{sel.moduleIds.map(mId => {
|
{sel.moduleIds.map(mId => {
|
||||||
const mod = prod.modules?.find(m => m.id === mId)
|
const mod = prod.modules?.find(m => m.id === mId)
|
||||||
|
const qty = moduleQuantities[mId] || 1
|
||||||
return mod ? (
|
return mod ? (
|
||||||
<div key={mId} className="flex justify-between text-xs pl-4">
|
<div key={mId} className="flex justify-between text-xs pl-4">
|
||||||
<span className="text-slate-300">+ {mod.name}</span>
|
<span className="text-slate-300">+ {mod.name} {mod.has_quantity ? `(x${qty})` : ''}</span>
|
||||||
<span className="text-slate-300">
|
<span className="text-slate-300">
|
||||||
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price)}
|
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(mod.price * qty)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
@@ -791,7 +834,8 @@ export function OrderWizard({
|
|||||||
Number(prod.base_price) +
|
Number(prod.base_price) +
|
||||||
sel.moduleIds.reduce((acc, mId) => {
|
sel.moduleIds.reduce((acc, mId) => {
|
||||||
const mod = prod.modules?.find(m => m.id === mId)
|
const mod = prod.modules?.find(m => m.id === mId)
|
||||||
return acc + Number(mod?.price ?? 0)
|
const qty = moduleQuantities[mId] || 1
|
||||||
|
return acc + (Number(mod?.price ?? 0) * qty)
|
||||||
}, 0)
|
}, 0)
|
||||||
return (
|
return (
|
||||||
<div key={cat.id}>
|
<div key={cat.id}>
|
||||||
@@ -808,7 +852,11 @@ export function OrderWizard({
|
|||||||
{sel.moduleIds.length > 0 && (
|
{sel.moduleIds.length > 0 && (
|
||||||
<p className="text-sm text-slate-300 mt-1 pl-6">
|
<p className="text-sm text-slate-300 mt-1 pl-6">
|
||||||
Module: {sel.moduleIds
|
Module: {sel.moduleIds
|
||||||
.map(id => prod.modules?.find(m => m.id === id)?.name)
|
.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})` : ''}` : ''
|
||||||
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(', ')}
|
.join(', ')}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ function hashOrderSnapshot(snapshot: object): string {
|
|||||||
*/
|
*/
|
||||||
export async function submitOrder(params: {
|
export async function submitOrder(params: {
|
||||||
selections: WizardSelections
|
selections: WizardSelections
|
||||||
|
moduleQuantities?: Record<string, number>
|
||||||
products?: Product[]
|
products?: Product[]
|
||||||
categories?: Category[]
|
categories?: Category[]
|
||||||
customerProfile: Partial<Profile>
|
customerProfile: Partial<Profile>
|
||||||
@@ -57,7 +58,7 @@ export async function submitOrder(params: {
|
|||||||
endCustomer?: EndCustomer | null
|
endCustomer?: EndCustomer | null
|
||||||
billingInterval?: 'one_time' | 'monthly'
|
billingInterval?: 'one_time' | 'monthly'
|
||||||
}): Promise<Order> {
|
}): Promise<Order> {
|
||||||
const { selections, customerProfile, endCustomerId, endCustomer, billingInterval } = params
|
const { selections, moduleQuantities, customerProfile, endCustomerId, endCustomer, billingInterval } = params
|
||||||
const supabase = await createClient()
|
const supabase = await createClient()
|
||||||
|
|
||||||
const { data: { user } } = await supabase.auth.getUser()
|
const { data: { user } } = await supabase.auth.getUser()
|
||||||
@@ -106,7 +107,7 @@ export async function submitOrder(params: {
|
|||||||
// 1. Snapshots aufbauen
|
// 1. Snapshots aufbauen
|
||||||
// Wenn Endkunde vorhanden: dessen Daten einfrieren; sonst Partner-Profil (Fallback)
|
// Wenn Endkunde vorhanden: dessen Daten einfrieren; sonst Partner-Profil (Fallback)
|
||||||
const customerSnapshot = buildCustomerSnapshot(customerProfile, endCustomer ?? null)
|
const customerSnapshot = buildCustomerSnapshot(customerProfile, endCustomer ?? null)
|
||||||
const orderSnapshot = buildOrderSnapshot(selections, dbProducts, dbCategories, billingInterval)
|
const orderSnapshot = buildOrderSnapshot(selections, dbProducts, dbCategories, billingInterval, moduleQuantities)
|
||||||
|
|
||||||
// 2. Idempotenz-Guard: Prüfen ob identische Bestellung in den letzten 30s existiert
|
// 2. Idempotenz-Guard: Prüfen ob identische Bestellung in den letzten 30s existiert
|
||||||
const orderHash = hashOrderSnapshot(orderSnapshot)
|
const orderHash = hashOrderSnapshot(orderSnapshot)
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export async function createProduct(
|
|||||||
price: m.price,
|
price: m.price,
|
||||||
requirements: m.requirements || [],
|
requirements: m.requirements || [],
|
||||||
exclusions: m.exclusions || [],
|
exclusions: m.exclusions || [],
|
||||||
|
has_quantity: m.has_quantity ?? false,
|
||||||
}))
|
}))
|
||||||
const { error: modulesError } = await supabase
|
const { error: modulesError } = await supabase
|
||||||
.from('product_modules')
|
.from('product_modules')
|
||||||
@@ -125,6 +126,7 @@ export async function updateProduct(id: string, product: Partial<Product>, modul
|
|||||||
price: m.price,
|
price: m.price,
|
||||||
requirements: m.requirements || [],
|
requirements: m.requirements || [],
|
||||||
exclusions: m.exclusions || [],
|
exclusions: m.exclusions || [],
|
||||||
|
has_quantity: m.has_quantity ?? false,
|
||||||
}))
|
}))
|
||||||
const { error: modulesError } = await supabase
|
const { error: modulesError } = await supabase
|
||||||
.from('product_modules')
|
.from('product_modules')
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ export function buildOrderSnapshot(
|
|||||||
selections: WizardSelections,
|
selections: WizardSelections,
|
||||||
products: Product[],
|
products: Product[],
|
||||||
categories: Category[],
|
categories: Category[],
|
||||||
billingInterval?: 'one_time' | 'monthly'
|
billingInterval?: 'one_time' | 'monthly',
|
||||||
|
moduleQuantities?: Record<string, number>
|
||||||
): OrderSnapshot {
|
): OrderSnapshot {
|
||||||
const items: OrderItem[] = []
|
const items: OrderItem[] = []
|
||||||
let subtotal = 0
|
let subtotal = 0
|
||||||
@@ -103,13 +104,18 @@ export function buildOrderSnapshot(
|
|||||||
const selectedModules = sel.moduleIds
|
const selectedModules = sel.moduleIds
|
||||||
.map((modId) => prod.modules?.find((m) => m.id === modId))
|
.map((modId) => prod.modules?.find((m) => m.id === modId))
|
||||||
.filter((m): m is NonNullable<typeof m> => !!m)
|
.filter((m): m is NonNullable<typeof m> => !!m)
|
||||||
.map((mod) => ({
|
.map((mod) => {
|
||||||
module_id: mod.id,
|
const qty = moduleQuantities?.[mod.id] || 1
|
||||||
module_name: mod.name,
|
return {
|
||||||
price: mod.price,
|
module_id: mod.id,
|
||||||
}))
|
module_name: mod.name,
|
||||||
|
price: mod.price,
|
||||||
|
quantity: qty,
|
||||||
|
total_price: mod.price * qty,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const moduleTotal = selectedModules.reduce((acc, m) => acc + m.price, 0)
|
const moduleTotal = selectedModules.reduce((acc, m) => acc + (m.total_price || m.price), 0)
|
||||||
const itemTotal = prod.base_price + moduleTotal
|
const itemTotal = prod.base_price + moduleTotal
|
||||||
subtotal += itemTotal
|
subtotal += itemTotal
|
||||||
|
|
||||||
@@ -157,7 +163,8 @@ export function transformToLicenseBundle(
|
|||||||
selections: WizardSelections,
|
selections: WizardSelections,
|
||||||
products: Product[],
|
products: Product[],
|
||||||
categories: Category[],
|
categories: Category[],
|
||||||
customer: Partial<Profile>
|
customer: Partial<Profile>,
|
||||||
|
moduleQuantities?: Record<string, number>
|
||||||
): LicenseBundle {
|
): LicenseBundle {
|
||||||
const options: LicenseOption[] = []
|
const options: LicenseOption[] = []
|
||||||
let monthlyTotal = 0
|
let monthlyTotal = 0
|
||||||
@@ -186,6 +193,7 @@ export function transformToLicenseBundle(
|
|||||||
for (const modId of sel.moduleIds) {
|
for (const modId of sel.moduleIds) {
|
||||||
const mod = prod.modules?.find((m) => m.id === modId)
|
const mod = prod.modules?.find((m) => m.id === modId)
|
||||||
if (!mod) continue
|
if (!mod) continue
|
||||||
|
const qty = moduleQuantities?.[mod.id] || 1
|
||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
key: `${prodKey}__${slugify(mod.name)}`,
|
key: `${prodKey}__${slugify(mod.name)}`,
|
||||||
@@ -194,10 +202,11 @@ export function transformToLicenseBundle(
|
|||||||
// Module erben das Billing-Intervall des übergeordneten Produkts
|
// Module erben das Billing-Intervall des übergeordneten Produkts
|
||||||
billing: prod.billing_interval,
|
billing: prod.billing_interval,
|
||||||
price_net: mod.price,
|
price_net: mod.price,
|
||||||
|
quantity: qty,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (prod.billing_interval === 'monthly') monthlyTotal += mod.price
|
if (prod.billing_interval === 'monthly') monthlyTotal += mod.price * qty
|
||||||
else oneTimeTotal += mod.price
|
else oneTimeTotal += mod.price * qty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,16 +247,19 @@ export function licenseBundleFromSnapshot(
|
|||||||
else oneTimeTotal += item.base_price
|
else oneTimeTotal += item.base_price
|
||||||
|
|
||||||
for (const mod of item.selected_modules) {
|
for (const mod of item.selected_modules) {
|
||||||
|
const qty = mod.quantity || 1
|
||||||
|
const modPrice = mod.total_price || (mod.price * qty)
|
||||||
options.push({
|
options.push({
|
||||||
key: `${prodKey}__${slugify(mod.module_name)}`,
|
key: `${prodKey}__${slugify(mod.module_name)}`,
|
||||||
label: mod.module_name,
|
label: mod.module_name,
|
||||||
active: true,
|
active: true,
|
||||||
billing: item.billing_interval,
|
billing: item.billing_interval,
|
||||||
price_net: mod.price,
|
price_net: mod.price,
|
||||||
|
quantity: qty,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (item.billing_interval === 'monthly') monthlyTotal += mod.price
|
if (item.billing_interval === 'monthly') monthlyTotal += modPrice
|
||||||
else oneTimeTotal += mod.price
|
else oneTimeTotal += modPrice
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export type ProductModule = {
|
|||||||
requirements: string[] // UUID[] anderer Module, die aktiv sein müssen
|
requirements: string[] // UUID[] anderer Module, die aktiv sein müssen
|
||||||
exclusions: string[] // UUID[] von Modulen, die nicht gleichzeitig aktiv sein dürfen
|
exclusions: string[] // UUID[] von Modulen, die nicht gleichzeitig aktiv sein dürfen
|
||||||
created_at: string
|
created_at: string
|
||||||
|
has_quantity?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Profile = {
|
export type Profile = {
|
||||||
@@ -103,6 +104,8 @@ export type OrderModuleSnapshot = {
|
|||||||
module_id: string
|
module_id: string
|
||||||
module_name: string
|
module_name: string
|
||||||
price: number
|
price: number
|
||||||
|
quantity?: number
|
||||||
|
total_price?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrderItem = {
|
export type OrderItem = {
|
||||||
@@ -154,6 +157,7 @@ export type LicenseOption = {
|
|||||||
active: boolean
|
active: boolean
|
||||||
billing: BillingInterval
|
billing: BillingInterval
|
||||||
price_net: number
|
price_net: number
|
||||||
|
quantity?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LicenseBundle = {
|
export type LicenseBundle = {
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add has_quantity column to product_modules table
|
||||||
|
ALTER TABLE public.product_modules ADD COLUMN IF NOT EXISTS has_quantity BOOLEAN DEFAULT false;
|
||||||
Reference in New Issue
Block a user