import { useEffect, useMemo, useState } from 'react' import { createPortal } from 'react-dom' import { Plus, X } from 'lucide-react' import { ApiError } from '../lib/api' import { listAllProducts } from '../services/productService' import { getProductVariationValues, type ProductVariationSelection, } from '../services/productVariationService' import { batchCreateStoreItems, type CreateStoreItemPayload, } from '../services/storeItemService' import { createId } from '../utils/id' import { formatIrtInput, parseIrtInput } from '../utils/irtPrice' import { createEmptyStoreItemRow, getVariationOptions, type StoreItemDraftRow, } from '../utils/storeItemRows' import { ProductSearchSelect } from './ProductSearchSelect' import { Tooltip } from './Tooltip' import modalStyles from './VariationsModal.module.css' import rowStyles from './CreateStoreItemsModal.module.css' interface CreateStoreItemsModalProps { open: boolean existingProductIds: string[] onClose: () => void onCreated?: () => void onEditExisting?: (productId: string) => void } const ANIMATION_MS = 220 export function CreateStoreItemsModal({ open, existingProductIds, onClose, onCreated, onEditExisting, }: CreateStoreItemsModalProps) { const [mounted, setMounted] = useState(open) const [closing, setClosing] = useState(false) const [products, setProducts] = useState<{ id: string; title: string; nameFa: string }[]>([]) const [productId, setProductId] = useState('') const [variations, setVariations] = useState([]) const [rows, setRows] = useState([]) const [isLoadingProducts, setIsLoadingProducts] = useState(false) const [isLoadingVariations, setIsLoadingVariations] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) const [error, setError] = useState('') const formVariations = useMemo( () => variations.filter((variation) => getVariationOptions(variation).length > 0), [variations], ) const gridTemplate = useMemo(() => { const variationCols = formVariations.map(() => 'minmax(110px, 1fr)').join(' ') const cols = [variationCols, 'minmax(120px, 1fr)', '90px', '32px'].filter((part) => part).join(' ') return cols || 'minmax(120px, 1fr) 90px 32px' }, [formVariations]) useEffect(() => { if (open) { setMounted(true) setClosing(false) } else if (mounted) { setClosing(true) const timer = setTimeout(() => { setMounted(false) setClosing(false) setProductId('') setVariations([]) setRows([]) setError('') }, ANIMATION_MS) return () => clearTimeout(timer) } }, [open, mounted]) useEffect(() => { if (!open) return const controller = new AbortController() void loadProducts(controller.signal) return () => controller.abort() }, [open]) useEffect(() => { if (!productId) { setVariations([]) setRows([]) return } const controller = new AbortController() void loadVariations(productId, controller.signal) return () => controller.abort() }, [productId]) async function loadProducts(signal?: AbortSignal) { setIsLoadingProducts(true) setError('') try { const items = await listAllProducts(signal) setProducts( items.map((item) => ({ id: item.id, title: item.title, nameFa: item.nameFa, })), ) } catch (err) { if (err instanceof DOMException && err.name === 'AbortError') return if (err instanceof ApiError) { setError(err.message) } else { setError('Unable to load products.') } } finally { setIsLoadingProducts(false) } } async function loadVariations(nextProductId: string, signal?: AbortSignal) { setIsLoadingVariations(true) setError('') try { const data = await getProductVariationValues(nextProductId, signal) setVariations(data.variations) setRows([createEmptyStoreItemRow()]) } catch (err) { if (err instanceof DOMException && err.name === 'AbortError') return if (err instanceof ApiError) { setError(err.message) } else { setError('Unable to load product variations.') } setVariations([]) setRows([]) } finally { setIsLoadingVariations(false) } } 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]) if (!mounted) return null function handleProductChange(nextProductId: string) { if (nextProductId && existingProductIds.includes(nextProductId)) { onEditExisting?.(nextProductId) onClose() return } setProductId(nextProductId) } function updateRow(rowId: string, patch: Partial) { setRows((prev) => prev.map((row) => (row.id === rowId ? { ...row, ...patch } : row))) } function updateRowSelection(rowId: string, variationId: string, optionId: string) { setRows((prev) => prev.map((row) => { if (row.id !== rowId) return row const nextSelections = { ...row.selections } if (!optionId) { delete nextSelections[variationId] } else { nextSelections[variationId] = optionId } return { ...row, selections: nextSelections } }), ) } function addRow() { setRows((prev) => [...prev, { ...createEmptyStoreItemRow(), id: createId() }]) } function removeRow(rowId: string) { setRows((prev) => (prev.length <= 1 ? prev : prev.filter((row) => row.id !== rowId))) } function buildPayload(): CreateStoreItemPayload[] { return rows.map((row) => ({ selections: formVariations.flatMap((variation) => { const optionId = row.selections[variation.id] if (!optionId) return [] return [{ variationId: variation.id, optionId }] }), price: parseIrtInput(row.price) ?? undefined, stockQuantity: row.stock.trim() ? Number(row.stock) : undefined, })) } const canSubmit = Boolean(productId) && rows.length > 0 && rows.every((row) => { const price = parseIrtInput(row.price) const stock = Number(row.stock) return price !== null && price >= 0 && row.stock.trim() !== '' && !Number.isNaN(stock) && stock >= 0 }) && !isSubmitting && !isLoadingVariations async function handleSubmit(e: React.FormEvent) { e.preventDefault() if (!canSubmit) return setIsSubmitting(true) setError('') try { await batchCreateStoreItems({ productId, items: buildPayload(), }) onCreated?.() onClose() } catch (err) { if (err instanceof ApiError) { setError(err.message) } else { setError('Unable to create store items.') } } finally { setIsSubmitting(false) } } return createPortal(
e.stopPropagation()} role="dialog" aria-modal="true" aria-labelledby="create-store-items-title" >

Add Store Items

Create sellable variants from a product's variations.

void handleSubmit(e)}>
{productId && isLoadingVariations && (

Loading product variations...

)} {productId && !isLoadingVariations && ( <>
{formVariations.map((variation) => ( {variation.name} ))} Price (IRT) Stock
{rows.map((row) => (
{formVariations.map((variation) => { const options = getVariationOptions(variation) return ( ) })} updateRow(row.id, { price: formatIrtInput(e.target.value) })} placeholder="0" disabled={isSubmitting} /> updateRow(row.id, { stock: e.target.value })} disabled={isSubmitting} />
))}
)} {error &&

{error}

}
, document.body, ) }