From af261ce9231a7a335665963d15ebc8883f312cf0 Mon Sep 17 00:00:00 2001 From: DanielS Date: Mon, 10 Aug 2026 23:37:09 +0200 Subject: [PATCH] feat: add unique license number check for devices --- shop/app/actions/checkout.ts | 3 +- shop/app/api/orders/checkout/route.ts | 3 +- shop/components/order-wizard.tsx | 57 +++++++++++++++++++++- shop/components/wizard/summary-sidebar.tsx | 19 +++++++- shop/lib/actions/queries.ts | 25 ++++++++++ 5 files changed, 103 insertions(+), 4 deletions(-) diff --git a/shop/app/actions/checkout.ts b/shop/app/actions/checkout.ts index d6d591e..4b76014 100644 --- a/shop/app/actions/checkout.ts +++ b/shop/app/actions/checkout.ts @@ -262,7 +262,8 @@ export async function checkoutAction(params: { ); const itemsWithDevice = itemSnapshot.items.map(i => ({ ...i, - device_name: item.deviceName + device_name: item.deviceName, + license_number: item.licenseNumber })); orderItemsList.push(...itemsWithDevice); total += itemSnapshot.total; diff --git a/shop/app/api/orders/checkout/route.ts b/shop/app/api/orders/checkout/route.ts index c407a73..46ff747 100644 --- a/shop/app/api/orders/checkout/route.ts +++ b/shop/app/api/orders/checkout/route.ts @@ -90,7 +90,8 @@ export async function POST(request: Request) { ); const itemsWithDevice = itemSnapshot.items.map(i => ({ ...i, - device_name: item.deviceName + device_name: item.deviceName, + license_number: item.licenseNumber })); orderItemsList.push(...itemsWithDevice); total += itemSnapshot.total; diff --git a/shop/components/order-wizard.tsx b/shop/components/order-wizard.tsx index 256be4c..8ca45f0 100644 --- a/shop/components/order-wizard.tsx +++ b/shop/components/order-wizard.tsx @@ -6,6 +6,7 @@ import { AnimatePresence, motion } from 'framer-motion' import { Product, ProductModule, Profile, EndCustomer, Order } from '@/lib/types' import { submitOrder, updateOrder } from '@/lib/actions/orders' import { createEndCustomer } from '@/lib/actions/end-customers' +import { isLicenseNumberTaken } from '@/lib/actions/queries' import { Category } from '@/lib/types' import { Check } from 'lucide-react' @@ -166,6 +167,7 @@ export function OrderWizard({ if (upgradeMode && lockedDeviceId) return `${lockedDeviceId} – Upgrade` return '' }) + const [licenseNumber, setLicenseNumber] = useState('') const [editingIdx, setEditingIdx] = useState(null) const [orderNotes, setOrderNotes] = useState('') const [toast, setToast] = useState<{ message: string; type: 'error' | 'success' } | null>(null) @@ -593,9 +595,27 @@ export function OrderWizard({ } } - const addToBasket = () => { + const addToBasket = async () => { + const normLicense = licenseNumber.trim().toUpperCase() + if (normLicense) { + const isDuplicateInBasket = basketItems.some( + (item, idx) => idx !== editingIdx && item.licenseNumber?.toUpperCase().trim() === normLicense + ) + if (isDuplicateInBasket) { + setToast({ message: `Lizenznummer "${licenseNumber}" ist bereits im aktuellen Warenkorb vorhanden.`, type: 'error' }) + return + } + + const isTaken = await isLicenseNumberTaken(normLicense, initialOrder?.id) + if (isTaken) { + setToast({ message: `Lizenznummer "${licenseNumber}" wird bereits von einer anderen Bestellung verwendet.`, type: 'error' }) + return + } + } + const currentItem = { deviceName: deviceName || (editingIdx !== null ? basketItems[editingIdx]?.deviceName : `Kasse ${basketItems.length + 1}`), + licenseNumber: normLicense, selections: JSON.parse(JSON.stringify(selections)), moduleQuantities: { ...moduleQuantities }, billingInterval: selectedBillingInterval, @@ -614,6 +634,7 @@ export function OrderWizard({ // Reset current form config setDeviceName('') + setLicenseNumber('') setModuleQuantities({}) const resetSels: Record = {} categories.forEach(cat => { @@ -636,6 +657,7 @@ export function OrderWizard({ setSelections(item.selections) setModuleQuantities(item.moduleQuantities) setDeviceName(item.deviceName) + setLicenseNumber(item.licenseNumber || '') setSelectedBillingInterval(item.billingInterval) } @@ -644,6 +666,7 @@ export function OrderWizard({ setEditingIdx(null) setModuleQuantities({}) setDeviceName('') + setLicenseNumber('') const resetSels: Record = {} categories.forEach(cat => { @@ -668,8 +691,24 @@ export function OrderWizard({ try { let finalItems = [...basketItems] if (hasActiveSelection && allCategoriesFilled && productValidationErrors.length === 0) { + const normLicense = licenseNumber.trim().toUpperCase() + if (normLicense) { + const isDuplicateInBasket = basketItems.some( + (item, idx) => idx !== editingIdx && item.licenseNumber?.toUpperCase().trim() === normLicense + ) + if (isDuplicateInBasket) { + throw new Error(`Lizenznummer "${licenseNumber}" ist bereits im aktuellen Warenkorb vorhanden.`) + } + + const isTaken = await isLicenseNumberTaken(normLicense, initialOrder?.id) + if (isTaken) { + throw new Error(`Lizenznummer "${licenseNumber}" wird bereits von einer anderen Bestellung verwendet.`) + } + } + const currentItem = { deviceName: deviceName || (editingIdx !== null ? basketItems[editingIdx]?.deviceName : `Kasse ${basketItems.length + 1}`), + licenseNumber: normLicense, selections: JSON.parse(JSON.stringify(selections)), moduleQuantities: { ...moduleQuantities }, billingInterval: selectedBillingInterval, @@ -685,6 +724,20 @@ export function OrderWizard({ throw new Error('Bitte konfigurieren Sie mindestens eine Kasse.') } + // Check all license numbers in finalItems for duplicates + const licenseNumbers = finalItems.map(item => item.licenseNumber?.toUpperCase().trim()).filter(Boolean) + const uniqueLicenses = new Set(licenseNumbers) + if (uniqueLicenses.size !== licenseNumbers.length) { + throw new Error('Eine Lizenznummer darf im Warenkorb nicht mehrfach verwendet werden.') + } + + for (const lic of uniqueLicenses) { + const isTaken = await isLicenseNumberTaken(lic, initialOrder?.id) + if (isTaken) { + throw new Error(`Die Lizenznummer "${lic}" wird bereits von einer anderen Bestellung verwendet.`) + } + } + const res = await fetch('/api/orders/checkout', { method: 'POST', headers: { @@ -1002,6 +1055,8 @@ export function OrderWizard({ deleteBasketItem={deleteBasketItem} deviceName={deviceName} setDeviceName={setDeviceName} + licenseNumber={licenseNumber} + setLicenseNumber={setLicenseNumber} addToBasket={addToBasket} isNextStepDisabled={isNextStepDisabled} hasActiveSelection={hasActiveSelection} diff --git a/shop/components/wizard/summary-sidebar.tsx b/shop/components/wizard/summary-sidebar.tsx index 16187f4..98ddbdc 100644 --- a/shop/components/wizard/summary-sidebar.tsx +++ b/shop/components/wizard/summary-sidebar.tsx @@ -32,6 +32,8 @@ interface SummarySidebarProps { deleteBasketItem: (idx: number) => void deviceName: string setDeviceName: (name: string) => void + licenseNumber: string + setLicenseNumber: (num: string) => void addToBasket: () => void isNextStepDisabled: boolean hasActiveSelection: boolean @@ -62,6 +64,8 @@ export function SummarySidebar({ deleteBasketItem, deviceName, setDeviceName, + licenseNumber, + setLicenseNumber, addToBasket, isNextStepDisabled, hasActiveSelection, @@ -231,7 +235,10 @@ export function SummarySidebar({
{item.deviceName} - {item.billingInterval === 'one_time' ? 'Kauf' : 'Abo'} + + {item.billingInterval === 'one_time' ? 'Kauf' : 'Abo'} + {item.licenseNumber ? ` · Lizenz: ${item.licenseNumber}` : ''} +
+
+ + setLicenseNumber(e.target.value)} + className="bg-white/5 border-white/10 text-white text-xs h-9 rounded-lg" + /> +