import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { X } from 'lucide-react' import { useLocale } from '@meshkee/dashboard-ui' import { useT } from '../i18n/useT' import { ApiError } from '../lib/api' import { listProducts } from '../services/productService' import { listStoreItems } from '../services/storeItemService' import type { SpecialProductsSource } from '../services/settingsService' import { groupStoreItemsByProduct } from '../utils/storeProductGroups' import { SEARCH_RESULT_LIMIT, StoreItemMultiSearchSelect, type StoreItemSearchOption, } from './StoreItemMultiSearchSelect' import modalStyles from './VariationsModal.module.css' import styles from './PickStoreSpecialItemsModal.module.css' interface PickStoreSpecialItemsModalProps { open: boolean specialTitle: string /** Store item ids when source is store_item; product ids when source is product. */ existingIds: string[] source: SpecialProductsSource onClose: () => void onConfirm: (ids: string[]) => void | Promise isSubmitting?: boolean } const ANIMATION_MS = 220 const STORE_ITEM_SEARCH_PAGE_SIZE = 30 export function PickStoreSpecialItemsModal({ open, specialTitle, existingIds, source, onClose, onConfirm, isSubmitting = false, }: PickStoreSpecialItemsModalProps) { const t = useT() const { locale } = useLocale() const isFa = locale === 'fa' const fromProducts = source === 'product' const [mounted, setMounted] = useState(open) const [closing, setClosing] = useState(false) const [options, setOptions] = useState([]) const [isSearching, setIsSearching] = useState(false) const [error, setError] = useState('') const [selectedIds, setSelectedIds] = useState([]) const searchControllerRef = useRef(null) const existingSet = useMemo(() => new Set(existingIds), [existingIds]) useEffect(() => { if (open) { setMounted(true) setClosing(false) setSelectedIds([]) setOptions([]) setError('') setIsSearching(false) } else if (mounted) { setClosing(true) const timer = setTimeout(() => { setMounted(false) setClosing(false) }, ANIMATION_MS) return () => clearTimeout(timer) } }, [open, mounted]) useEffect(() => { return () => { searchControllerRef.current?.abort() } }, []) useEffect(() => { if (!mounted || closing) return const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() } document.addEventListener('keydown', onKey) return () => document.removeEventListener('keydown', onKey) }, [mounted, closing, onClose]) const handleSearch = useCallback( async (query: string) => { searchControllerRef.current?.abort() if (!query) { setOptions([]) setIsSearching(false) setError('') return } const controller = new AbortController() searchControllerRef.current = controller setIsSearching(true) setError('') try { const nextOptions = fromProducts ? await loadProductOptions(query, controller.signal) : await loadStoreItemOptions(query, controller.signal) if (controller.signal.aborted) return setOptions(nextOptions.filter((option) => !existingSet.has(option.id))) } catch (err) { if (err instanceof DOMException && err.name === 'AbortError') return if (controller.signal.aborted) return if (err instanceof ApiError) { setError(err.message) } else { setError( fromProducts ? t('website.specialItems.pickErrorLoadProducts') : t('website.specialItems.pickErrorLoad'), ) } } finally { if (!controller.signal.aborted) setIsSearching(false) } }, [existingSet, fromProducts, t], ) function toggleSelection(id: string) { setSelectedIds((prev) => prev.includes(id) ? prev.filter((entry) => entry !== id) : [...prev, id], ) } async function handleConfirm() { if (selectedIds.length === 0 || isSubmitting) return setError('') try { await onConfirm(selectedIds) } catch (err) { if (err instanceof ApiError) { setError(err.message) } else if (err instanceof Error && err.message) { setError(err.message) } else { setError( t('website.specialItems.errorAdd'), ) } } } function confirmLabel() { if (isSubmitting) return t('website.adding') if (selectedIds.length === 1) return t('website.pick.addOneItem') return t('website.pick.addItems', { count: selectedIds.length }) } if (!mounted) return null return createPortal(
{ if (e.target !== e.currentTarget) return if (selectedIds.length > 0) return onClose() }} role="presentation" >
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} >

{t('website.pick.addTo', { title: specialTitle })}

{fromProducts ? t('website.specialItems.pickSubtitleProducts') : t('website.specialItems.pickSubtitle')}

{error &&

{error}

} void handleSearch(query)} isSearching={isSearching} placeholder={ fromProducts ? t('website.specialItems.pickSearchProducts') : t('website.specialItems.pickSearch') } disabled={isSubmitting} />
, document.body, ) } async function loadProductOptions(name: string, signal?: AbortSignal) { const data = await listProducts(1, SEARCH_RESULT_LIMIT, signal, { name }) return data.items.map( (product): StoreItemSearchOption => ({ id: product.id, title: product.nameEn, nameFa: product.nameFa, image: product.thumbnail || product.image || null, }), ) } async function loadStoreItemOptions(name: string, signal?: AbortSignal) { const data = await listStoreItems(1, STORE_ITEM_SEARCH_PAGE_SIZE, signal, { name }) const listings = groupStoreItemsByProduct(data.items) return listings .map((listing): StoreItemSearchOption | null => { const storeItemId = listing.representative.storeItemId ?? listing.representative.id const firstPricedVariant = listing.variants.find((v) => v.price !== null) ?? null const price = firstPricedVariant?.price ?? null const hasPrice = listing.variants.some((v) => v.price !== null) const hasInStock = listing.variants.some( (v) => v.stockQuantity === null || v.stockQuantity > 0, ) if (!hasPrice && !hasInStock) return null return { id: storeItemId, title: listing.productTitle, nameFa: listing.productNameFa, image: listing.productImage, price, } }) .filter((option): option is StoreItemSearchOption => option !== null) .slice(0, SEARCH_RESULT_LIMIT) }