feat: add update price modifier for existing customers based on last license date
Some checks failed
Staging Build / build (push) Has been cancelled

This commit is contained in:
DanielS
2026-06-25 23:04:48 +02:00
parent 16fb713665
commit 1a2ace937c
6 changed files with 182 additions and 13 deletions

View File

@@ -113,6 +113,37 @@ export function OrderWizard({
// Modulmengen (moduleId -> Menge)
const [moduleQuantities, setModuleQuantities] = useState<Record<string, number>>({})
// Datum der letzten Lizenz (Bestandskunden-Update-Logik)
const [lastLicenseDate, setLastLicenseDate] = useState<string>("")
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])
// Per-category selection map: categoryId -> { productId, moduleIds }
const [selections, setSelections] = useState<Record<string, CategorySelection>>(() => {
const init: Record<string, CategorySelection> = {}
@@ -203,8 +234,12 @@ export function OrderWizard({
monthly += totalItem
}
}
// Apply update modifier to oneTime total
if (updatePriceModifier.multiplier !== 1) {
oneTime = oneTime * updatePriceModifier.multiplier
}
return { monthlyTotal: monthly, oneTimeTotal: oneTime }
}, [selections, products, categories, moduleQuantities])
}, [selections, products, categories, moduleQuantities, updatePriceModifier])
// Helper: update product selection for a category (resets modules)
function selectProduct(catId: string, productId: string) {
@@ -280,6 +315,7 @@ export function OrderWizard({
endCustomerId: selectedEndCustomerId,
endCustomer: selectedEndCustomer,
billingInterval: selectedBillingInterval,
lastLicenseDate: lastLicenseDate || null,
})
router.push(`/order/success?id=${order.id}`)
} catch (error) {
@@ -401,6 +437,24 @@ export function OrderWizard({
))}
</div>
)}
{selectedEndCustomerId && (
<div className="p-4 rounded-xl bg-white/5 border border-white/10 space-y-3 mt-4 max-w-md">
<Label htmlFor="last-license-date" className="text-white font-medium flex items-center gap-2">
<Calendar className="w-4 h-4 text-primary" />
Datum der letzten CAS-Lizenz (falls vorhanden)
</Label>
<Input
id="last-license-date"
type="date"
value={lastLicenseDate}
onChange={e => setLastLicenseDate(e.target.value)}
className="bg-[#0b1329] border-white/10 text-white focus:border-primary"
/>
<p className="text-xs text-slate-400">
Falls dieser Kunde bereits Lizenzen besitzt, tragen Sie das Datum der letzten Lizenzierung ein. Damit werden die korrekten Update-Gebühren ermittelt.
</p>
</div>
)}
</div>
)}
@@ -819,6 +873,15 @@ export function OrderWizard({
</span>
</div>
)}
{updatePriceModifier.label && (
<div className="text-xs text-green-400 bg-green-500/10 p-2.5 rounded-lg border border-green-500/20 space-y-1">
<p className="font-semibold flex items-center gap-1">
<Check className="w-3.5 h-3.5" />
Update-Rabatt aktiv
</p>
<p>{updatePriceModifier.label}</p>
</div>
)}
{!allCategoriesFilled && (
<p className="text-xs text-destructive flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
@@ -920,6 +983,12 @@ export function OrderWizard({
<p>{[selectedEndCustomer.first_name, selectedEndCustomer.last_name].filter(Boolean).join(' ')}</p>
<p>{[selectedEndCustomer.street, selectedEndCustomer.zip, selectedEndCustomer.city].filter(Boolean).join(', ')}</p>
<p className="text-xs text-slate-500 mt-1">Endkunde Ihres Partners</p>
{lastLicenseDate && (
<p className="text-xs text-slate-400 flex items-center gap-1 mt-1 font-mono">
<Calendar className="w-3.5 h-3.5 text-primary" />
Letzte Lizenz vom: {new Date(lastLicenseDate).toLocaleDateString('de-DE')}
</p>
)}
</>
) : (
<>
@@ -938,6 +1007,11 @@ export function OrderWizard({
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTotal)}
</span>
</div>
{updatePriceModifier.label && (
<div className="text-sm text-green-400 font-semibold bg-green-500/10 p-2 rounded border border-green-500/20 my-1">
{updatePriceModifier.label}
</div>
)}
<div className="flex justify-between text-lg font-bold text-white">
<span className="text-slate-400">Monatlicher Gesamtbetrag:</span>
<span>
@@ -947,11 +1021,18 @@ export function OrderWizard({
</div>
</div>
) : oneTimeTotal > 0 ? (
<div className="flex justify-between text-xl font-bold text-gradient border-t border-white/10 pt-4">
<span>Gesamtbetrag:</span>
<span>
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTotal)}
</span>
<div className="flex flex-col border-t border-white/10 pt-4 gap-1.5">
<div className="flex justify-between text-xl font-bold text-gradient">
<span>Gesamtbetrag:</span>
<span>
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(oneTimeTotal)}
</span>
</div>
{updatePriceModifier.label && (
<div className="text-sm text-green-400 font-semibold bg-green-500/10 p-2 rounded border border-green-500/20 self-end">
{updatePriceModifier.label}
</div>
)}
</div>
) : (
<div className="flex justify-between text-xl font-bold text-gradient border-t border-white/10 pt-4">