diff --git a/apps/dashboard/src/forms/fr_accounting/components/ProductOrderingCard.tsx b/apps/dashboard/src/forms/fr_accounting/components/ProductOrderingCard.tsx index 1574210f..f0fb6710 100644 --- a/apps/dashboard/src/forms/fr_accounting/components/ProductOrderingCard.tsx +++ b/apps/dashboard/src/forms/fr_accounting/components/ProductOrderingCard.tsx @@ -25,6 +25,43 @@ import { RegistrationSection } from "@/shared/vars" import { cn } from "@/utils/cx" import { formatCurrency } from "@/utils/format_currency" import { cx } from "class-variance-authority" +import { useRef, useState } from "react" + +const useDebouncedRequest = ( + defaultValue: T, + callback: (value: T) => Promise, + delay = 500 +): [T, (value: T) => void, boolean] => { + const [value, setValue] = useState(defaultValue) + const [loading, setLoading] = useState(false) + const currentPromise = useRef | null>(null) + const abortController = useRef(new AbortController()) + + const onChange = async (value: T) => { + setValue(value) + abortController.current.abort() + abortController.current = new AbortController() + setLoading(true) + + const currentController = abortController.current + + if (currentPromise.current != null) { + await currentPromise.current + } + + if (currentController.signal.aborted) return + + setTimeout(async () => { + if (currentController.signal.aborted) return + currentPromise.current = callback(value) + await currentPromise.current + if (currentController.signal.aborted) return + setLoading(false) + }, delay) + } + + return [value, onChange, loading] +} export function ProductOrderingCard({ product @@ -48,8 +85,6 @@ export function ProductOrderingCard({ const productAdjustedUnitPrice = (productWithAdjustedPrice?.adjustedPrice ?? 0) / (order?.quantity ?? 1) - const selected = orders.find(x => x.product.id === product.id) - const packageOrder = orders.find(order => belongsToSection(order.product, RegistrationSection.Packages) ) @@ -64,26 +99,32 @@ export function ProductOrderingCard({ current => current.child_product.id === product.id )?.quantity ?? 0 - async function onChange(quantity: number) { - if (isNaN(quantity)) quantity = 0 - if (product.max_quantity != null && quantity > product.max_quantity) - quantity = product.max_quantity - if (quantity <= 0 || quantity == null) { - await setProductOrder(product.id, 0) - } else if ( - !( - // If the product has a max of 1 and is already - // included, don't allow it to be added or incremented - ( - product.max_quantity != null && - product.max_quantity <= 1 && - packageProductBaseQuantity > 0 + const [quantity, setQuantity] = useDebouncedRequest( + order?.quantity ?? undefined, + async (quantity: number | undefined) => { + if (quantity == null || isNaN(quantity)) quantity = 0 + if (product.max_quantity != null && quantity > product.max_quantity) + quantity = product.max_quantity + if (quantity <= 0 || quantity == null) { + await setProductOrder(product.id, 0) + } else if ( + !( + // If the product has a max of 1 and is already + // included, don't allow it to be added or incremented + ( + product.max_quantity != null && + product.max_quantity <= 1 && + packageProductBaseQuantity > 0 + ) ) - ) - ) { - await setProductOrder(product.id, quantity) - } - } + ) { + await setProductOrder(product.id, quantity) + } + }, + // Seems to work fine with 0 seconds debounce + // Anyway I don't have time to remove the "debounce" functionality + 0 + ) const packageName = productPackage?.short_name || productPackage?.name || "package" @@ -91,7 +132,7 @@ export function ProductOrderingCard({ return ( @@ -111,10 +152,11 @@ export function ProductOrderingCard({ packageProductBaseQuantity <= 0 ? ( - onChange(value ? 1 : 0) + setQuantity(value ? 1 : 0) } checked={ - selected != null || + // order != null || + (quantity != null && quantity > 0) || packageProductBaseQuantity > 0 } disabled={packageProductBaseQuantity > 0} @@ -126,13 +168,20 @@ export function ProductOrderingCard({ - onChange(parseInt(event.target.value)) - } + onChange={event => { + if (event.target.value === "") { + setQuantity(undefined) + } else { + setQuantity( + parseInt(event.target.value) + ) + } + }} /> )} @@ -149,7 +198,7 @@ export function ProductOrderingCard({ packageProductBaseQuantity > 0 ? `Included In ${packageName}` : packageProductBaseQuantity > 0 && - (selected?.quantity ?? 0) <= 0 + (order?.quantity ?? 0) <= 0 ? "0 kr" : `${formatCurrency( productWithAdjustedPrice?.adjustedPrice ?? @@ -158,7 +207,7 @@ export function ProductOrderingCard({
- {selected != null && selected.quantity > 1 && ( + {order != null && order.quantity > 1 && (

Unit price

& { @@ -73,14 +74,17 @@ export function useAccountingMutation< * @param productId The id of the product you're picking * @param quantity How many instances of this product you want, 0 to remove it */ - async function setProductOrder(productId: number, quantity: number) { - const newOrders = orders.filter( - order => order.product.id !== productId - ) as OrderMutation[] as TVar - if (quantity > 0) - newOrders.push({ product: { id: productId }, quantity }) - await mutation.mutateAsync(newOrders) - } + const setProductOrder = useCallback( + async (productId: number, quantity: number) => { + const newOrders = orders.filter( + order => order.product.id !== productId + ) as OrderMutation[] as TVar + if (quantity > 0) + newOrders.push({ product: { id: productId }, quantity }) + await mutation.mutateAsync(newOrders) + }, + [mutation, orders] + ) return { ...mutation, diff --git a/apps/dashboard/src/forms/ir_signup/ir_registration.page.tsx b/apps/dashboard/src/forms/ir_signup/ir_registration.page.tsx index 2ecb8ecb..973f80a4 100644 --- a/apps/dashboard/src/forms/ir_signup/ir_registration.page.tsx +++ b/apps/dashboard/src/forms/ir_signup/ir_registration.page.tsx @@ -34,14 +34,15 @@ export default function IrRegistrationPage() { const { data, isLoading } = useDashboard() const accessDeclarationArgs = useAccessDeclaration() - const [readTerms, setReadTerms] = useState(false) - const [acceptedBinding, setAcceptedBinding] = useState(false) - const [processData, setProcessData] = useState(false) - const isFinalRegistration = checkAccessDeclarations(accessDeclarationArgs, [ "complete_registration:::*:::*" ]) + const [readTerms, setReadTerms] = useState(false) + const [acceptedBinding, setAcceptedBinding] = useState(false) + const [processData, setProcessData] = useState(false) + const [allowLateConfirmation, setAllowLateConfirmation] = useState(false) + const { mutate: signIr, isPending, @@ -108,7 +109,11 @@ export default function IrRegistrationPage() { } const signupDisabled = - !readTerms || !processData || !acceptedBinding || isPending + isPending || + !readTerms || + !processData || + (!isFinalRegistration && !acceptedBinding) || + (isFinalRegistration && !allowLateConfirmation) const acceptanceDate = DateTime.fromISO(dataDates.ir.acceptance).toFormat( "d MMMM yyyy" @@ -118,23 +123,26 @@ export default function IrRegistrationPage() { ) const frEndDate = DateTime.fromISO(dataDates.fr.end).toFormat("d MMMM yyyy") - // const overfillWarning = ( - // <> - // In order not to overfill the event, we will confirm your spot before{" "} - // {DateTime.fromISO(dataDates.ir.acceptance).toFormat("d MMMM")}. - // - // ) + const overfillWarning = isFinalRegistration ? ( + <> + Please note that the event may be full. We will contact you with a + confirmation of your spot as soon as possible. + + ) : ( + <> + In order not to overfill the event, we will confirm your spot before{" "} + {DateTime.fromISO(dataDates.ir.acceptance).toFormat("d MMMM")}. + + ) return (

Here you apply to participate in Armada{" "} - {DateTime.now().year}. {/* {overfillWarning} */} - Contact - sales@armada.nu - {" "} - if you have any questions + {DateTime.now().year}. {overfillWarning} Contact{" "} + sales@armada.nu if you + have any questions.

@@ -156,72 +164,95 @@ export default function IrRegistrationPage() { > I have read and accept the Terms and Conditions, and confirm that I have the right to enter this agreement on behalf of{" "} - {data?.company.name} - -
-
- - x != "indeterminate" && setAcceptedBinding(x) - } - /> -
+ {!isFinalRegistration && ( +
+ + x != "indeterminate" && setAcceptedBinding(x) + } + /> + +
+ )} + {isFinalRegistration && ( +
+ + x != "indeterminate" && setAllowLateConfirmation(x) + } + /> + +
+ )}
I consent to letting THS Armada store my personal - information according to THS personal information policy + information according to THS personal information policy.