/* eslint-disable @typescript-eslint/no-explicit-any */ import { CircularProgress } from '@mui/material' import { Form, Formik } from 'formik' import _identity from 'lodash/identity' import _map from 'lodash/map' import React, { useEffect, useMemo, useState } from 'react' import { Tooltip } from 'react-tooltip' import { getAddons, getCart } from '../../api' import { FEES_STYLES } from '../../constants' import { currencyNormalizerCreator } from '../../normalizers' import { CONFIGS, createMarkup } from '../../utils' import InfoIcon from '../ticketsContainer/InfoIcon' import { addonsWithGroupsAdapter, cartAdapter } from './adapters' import AddonComponent from './AddonComponent' import { getNormalizedPrice } from './normalizers' import { generateSelectOptions, getAddonSelectOptions, getSortedAddons, getTicketRelatedAddons, } from './utils' export interface ISimpleAddonContainerProps { classNamePrefix?: string; onGetAddonsPageInfoSuccess?: (res: any) => void; onGetAddonsPageInfoError?: (error: any) => void; descriptionTrigger?: 'click' | 'hover' | 'always'; addOnDataWithCustomFields: any; configs: any; eventId: string; useStepperQty?: boolean; maxTotalQuantity?: number; addonMaxQuantityGroups?: number; onAddOnSelect?: (id: string, value: string, addon: any, fieldUpdates: any) => void; handleConfirm?: (values: any) => void; } export interface ObjectLiteral { [key: string]: any; } export const SimpleAddonsContainer = ({ classNamePrefix = 'add_on', onGetAddonsPageInfoSuccess = _identity, onGetAddonsPageInfoError = _identity, descriptionTrigger = 'click', addOnDataWithCustomFields, configs, eventId, useStepperQty = false, addonMaxQuantityGroups, onAddOnSelect = _identity, handleConfirm = _identity, }: ISimpleAddonContainerProps) => { const [addons, setAddons] = useState(null) const [addonsOptions, setAddonsOptions] = useState({}) const [groupsWithSelectedVariants, setGroupsWithSelectedVariants] = useState({}) const [groupsWithInitialVariantsValues, setGroupsWithInitialVariantsValues] = useState({}) const [crossGroupSelections, setCrossGroupSelections] = useState({}) const [loading, setLoading] = useState(true) const [visibleDescription, setVisibleDescription] = useState(null) const handleDescriptionToggle = (ticketId: string) => { setVisibleDescription(current => (current === ticketId ? null : ticketId)) } // build { [fieldName]: value } for one addon from current Formik values const collectAddonFieldUpdates = (addon: any, allValues: Record) => { const out: Record = {} if (!addOnDataWithCustomFields?.fields?.length) return out addOnDataWithCustomFields.fields.forEach((group: any) => { group.groupItems.forEach((item: any) => { const k = `${addon.id}-${group.id}-${item.name}` if (k in allValues) out[item.name] = allValues[k] }) }) return out } // live-sync a single custom field change into localStorage.add_on_data_capture const handleCustomFieldChange = ( addon: any, _groupId: string | number, fieldName: string, value: any ) => { const existing = JSON.parse(localStorage.getItem('add_on_data_capture') || '{}') const addonId = String(addon.id) const next = { ...existing, [addonId]: { ...(existing[addonId] || {}), [fieldName]: value }, } localStorage.setItem('add_on_data_capture', JSON.stringify(next)) } useEffect(() => { const getAddonsPageInfo = async () => { try { if (eventId) { setLoading(true) // Get choosed ticket info (id, count) from Cart request for addons options calculations const cart = await getCart() const { id: choosedTicketID, quantity } = cartAdapter(cart) const choosedTicketCount = Number(quantity) // Get and collect addons data const addonsData = await getAddons(eventId) const adaptedAddons = addonsWithGroupsAdapter(addonsData) const ticketRelatedAddons = getTicketRelatedAddons( adaptedAddons, choosedTicketID ) const sortedTicketAddons = getSortedAddons(ticketRelatedAddons) setAddons(sortedTicketAddons) // Collect addons and addon group options const { addonsWithOptions, groupsWithSelectedVariantsInfo, groupsWithVariants, } = getAddonSelectOptions(adaptedAddons, choosedTicketCount) setAddonsOptions(addonsWithOptions) setGroupsWithSelectedVariants(groupsWithSelectedVariantsInfo) setGroupsWithInitialVariantsValues(groupsWithVariants) // Success callback props onGetAddonsPageInfoSuccess(addonsData) } } catch (e) { // Callback error props onGetAddonsPageInfoError(e) } finally { setLoading(false) } } getAddonsPageInfo() }, [eventId, onGetAddonsPageInfoError, onGetAddonsPageInfoSuccess]) const recreateGroupVariantsSelectOptions = (groupId: any, changedGroupd: any) => { const { choosedVariants, limit, selectedCount } = changedGroupd const remainingGroupStock = limit - selectedCount const recreatedVariantsOptions: ObjectLiteral = {} // Regenerate variants allowed stock counts for (const variant in choosedVariants) { const variantId = variant const variantCurrSelectedValue = choosedVariants[variant] let allowedOptionCount // Formula for regenerating if ( remainingGroupStock >= groupsWithInitialVariantsValues[groupId][variantId] - variantCurrSelectedValue ) { allowedOptionCount = groupsWithInitialVariantsValues[groupId][variantId] } else { allowedOptionCount = remainingGroupStock + variantCurrSelectedValue } recreatedVariantsOptions[variantId] = generateSelectOptions(0, allowedOptionCount) } setAddonsOptions((prevState: any) => Object.assign({}, prevState, recreatedVariantsOptions) ) } const onFieldChange = (id: any, value: any, addon: any) => { // If changeableGroup exsists it means that group with limitation variant was changed const changeableGroup = groupsWithSelectedVariants[addon.id] if (changeableGroup) { const currGroupId = addon.id const currSelectedVariantId = id const currSelectedVariantCount = Number(value) const currSelectedVariantPrevCount = groupsWithSelectedVariants[currGroupId].choosedVariants[currSelectedVariantId] const currSelectedGroupCount = changeableGroup.selectedCount + (currSelectedVariantCount - currSelectedVariantPrevCount) // Update Group info const updatedGroupsWithSelectedVariants = { ...groupsWithSelectedVariants, [currGroupId]: { ...groupsWithSelectedVariants[currGroupId], selectedCount: currSelectedGroupCount, choosedVariants: { ...groupsWithSelectedVariants[currGroupId].choosedVariants, [currSelectedVariantId]: currSelectedVariantCount, }, }, } setGroupsWithSelectedVariants(updatedGroupsWithSelectedVariants) // Recreate Select Options for Addon Group Variants recreateGroupVariantsSelectOptions( currGroupId, updatedGroupsWithSelectedVariants[currGroupId] ) } } // Cross-group cap: when addonMaxQuantityGroups is set, limits total selections // across ALL addon groups to that number by capping each stepper's max. const effectiveAddonsOptions = useMemo(() => { if (!addonMaxQuantityGroups) return addonsOptions const totalSelected = Object.values(crossGroupSelections).reduce( (sum: number, v: any) => sum + Number(v), 0 ) const globalRemaining = addonMaxQuantityGroups - totalSelected const result: ObjectLiteral = {} for (const id in addonsOptions) { const opts = addonsOptions[id] || [] const sdkMax = opts.length ? opts[opts.length - 1].value : 0 const currSelected = Number(crossGroupSelections[id] || 0) const allowedMax = Math.max(0, Math.min(sdkMax, globalRemaining + currSelected)) result[id] = opts.filter((opt: any) => opt.value <= allowedMax) } return result }, [addonsOptions, crossGroupSelections, addonMaxQuantityGroups]) const handleCrossGroupChange = (id: any, value: any) => { if (addonMaxQuantityGroups) { setCrossGroupSelections((prev: ObjectLiteral) => ({ ...prev, [id]: Number(value) })) } } const initialValues = useMemo(() => { const addOnsData: any = {} if (addons?.length > 0 && addOnDataWithCustomFields?.fields?.length > 0) { _map(addons, addon => { _map(addOnDataWithCustomFields.fields, field => { const { id, groupItems } = field _map(groupItems, item => { addOnsData[`${addon.id}-${id}-${item.name}`] = item.value }) }) }) } return addOnsData }, [addons, addOnDataWithCustomFields]) if (loading || !addons) { return (
) } return (
{addons?.length > 0 && (
UPGRADES & ADD-ONS
)}
{addons?.length > 0 && (
PLEASE SELECT FROM THE OPTIONAL ADD-ONS BELOW
)} { handleConfirm(values) }} validate={() => { // Real-time validation can be handled here if needed }} > {({ values, errors, setFieldTouched }) => { return (
<> {addons.map((addon: any) => { const price = addon.feeIncluded ? addon.price : addon.cost const isAddonFree = Number(addon?.price) === 0 const addOnNormalizedCost = isAddonFree ? 'FREE' : currencyNormalizerCreator( getNormalizedPrice(addon?.cost ?? 0), addon.currency ) const addonNormalizedPrice = isAddonFree ? 'FREE' : currencyNormalizerCreator( getNormalizedPrice( CONFIGS.FEES_STYLE === FEES_STYLES.DISPLAY_BOTH ? addon?.price ?? price : price ), addon.currency ) return (
{addon.imageUrl && (
No Data
)}
{addon.name} {addon.description && descriptionTrigger !== 'always' && ( <> handleDescriptionToggle(addon.id) : undefined } onMouseEnter={ descriptionTrigger === 'hover' ? () => setVisibleDescription(addon.id) : undefined } onMouseLeave={ descriptionTrigger === 'hover' ? () => setVisibleDescription(null) : undefined } style={{ marginLeft: 8, cursor: 'pointer', display: 'flex', }} data-tooltip-id={`tooltip-${addon.id}`} data-tooltip-content="View Add-On info" > {addon.description || 'No description available'} )}
{CONFIGS.FEES_STYLE === FEES_STYLES.TRADITIONAL ? addonNormalizedPrice : addOnNormalizedCost} {!isAddonFree && CONFIGS.FEES_STYLE === FEES_STYLES.TRADITIONAL && ( {addon.feeIncluded ? '(incl. Fees)' : '(excl. Fees)'} )} {!isAddonFree && CONFIGS.FEES_STYLE === FEES_STYLES.DISPLAY_BOTH && ( <> {`(${addonNormalizedPrice} with fees)`} )}
{(visibleDescription === addon.id || descriptionTrigger === 'always') && (
)}
{addon.variants ? ( addon.variants.map((variant: any) => ( // Group Variants v.id !== variant.id).map((v: any) => v.id)} handleAddonChange={(id, value) => { handleCrossGroupChange(id, value) onFieldChange(id, value, addon) const fieldUpdates = collectAddonFieldUpdates( addon, values ) onAddOnSelect(id, value, addon, fieldUpdates) }} onCustomFieldChange={handleCustomFieldChange} addOnDataWithCustomFields={addOnDataWithCustomFields} configs={configs} values={values} errors={errors} useStepperQty={useStepperQty} /> )) ) : ( // Simple Addon { handleCrossGroupChange(id, value) onFieldChange(id, value, addon) const fieldUpdates = collectAddonFieldUpdates(addon, values) onAddOnSelect(id, value, addon, fieldUpdates) _map(addOnDataWithCustomFields.fields, fieldGroup => { const { id, groupItems } = fieldGroup _map(groupItems, field => { setFieldTouched( `${addon.id}-${id}-${field.name}`, true, true ) }) }) }} onCustomFieldChange={handleCustomFieldChange} addOnDataWithCustomFields={addOnDataWithCustomFields} configs={configs} values={values} errors={errors} useStepperQty={useStepperQty} /> )}
) })} )}}
) } export default SimpleAddonsContainer