import { useEffect, useMemo, useState } from 'react' import { createPortal } from 'react-dom' import { Plus, X } from 'lucide-react' import { useLocale } from '@meshkee/dashboard-ui' import { useT } from '../i18n/useT' import { ApiError } from '../lib/api' import { getProductVariationValues, type ProductVariationSelection, } from '../services/productVariationService' import { syncProductStoreItems, type StoreItem, } from '../services/storeItemService' import { createId } from '../utils/id' import { formatIrtInput, parseIrtInput } from '../utils/irtPrice' import { createEmptyStoreItemRow, getVariationOptions, type StoreItemDraftRow, } from '../utils/storeItemRows' import { localizeVariationOptionLabel } from '../utils/localizeVariationOption' import { Tooltip } from './Tooltip' import modalStyles from './VariationsModal.module.css' import rowStyles from './CreateStoreItemsModal.module.css' interface EditStoreItemsModalProps { open: boolean productTitle: string productNameFa: string items: StoreItem[] onClose: () => void onSaved?: (items: StoreItem[]) => void } const ANIMATION_MS = 220 function itemsToRows(storeItems: StoreItem[]): StoreItemDraftRow[] { if (storeItems.length === 0) return [createEmptyStoreItemRow()] return storeItems.map((item) => { const selections: Record = {} for (const selection of item.selections) { selections[selection.variationId] = selection.optionId } const unlimited = item.stockQuantity === null return { id: createId(), storeItemId: item.id, selections, price: item.price !== null ? formatIrtInput(String(item.price)) : '', stock: unlimited ? '1' : String(item.stockQuantity), unlimitedStock: unlimited, } }) } export function EditStoreItemsModal({ open, productTitle, productNameFa, items, onClose, onSaved, }: EditStoreItemsModalProps) { const t = useT() const { locale } = useLocale() const isFa = locale === 'fa' const [mounted, setMounted] = useState(open) const [closing, setClosing] = useState(false) const [variations, setVariations] = useState([]) const [rows, setRows] = useState([]) const [removedIds, setRemovedIds] = useState([]) const [isUsed, setIsUsed] = useState(false) const [isLoading, setIsLoading] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) const [error, setError] = useState('') const productId = items[0]?.productId ?? '' const formVariations = useMemo( () => variations.filter((variation) => getVariationOptions(variation).length > 0), [variations], ) // When modal loads, ensure each variation select has a valid default selection. // This fixes cases where existing store-item variants don't return selections for all variations. useEffect(() => { if (!mounted || closing) return if (formVariations.length === 0) return setRows((prev) => { let changed = false const next = prev.map((row) => { const nextSelections = { ...row.selections } for (const variation of formVariations) { const current = nextSelections[variation.id] if (current) continue const options = getVariationOptions(variation) const first = options[0] if (first?.id) { nextSelections[variation.id] = first.id changed = true } } return changed ? { ...row, selections: nextSelections } : row }) return changed ? next : prev }) }, [formVariations, mounted, closing]) const gridTemplate = useMemo(() => { const variationCols = formVariations.map(() => 'minmax(110px, 1fr)').join(' ') const cols = [variationCols, 'minmax(120px, 1fr)', '90px', '72px', '32px'] .filter((part) => part) .join(' ') return cols || 'minmax(120px, 1fr) 90px 72px 32px' }, [formVariations]) useEffect(() => { if (open && productId) { setMounted(true) setClosing(false) setRemovedIds([]) setRows(itemsToRows(items)) setIsUsed(items.some((item) => item.isUsed)) setError('') const controller = new AbortController() void loadVariations(productId, controller.signal) return () => controller.abort() } if (mounted && !open) { setClosing(true) const timer = setTimeout(() => { setMounted(false) setClosing(false) }, ANIMATION_MS) return () => clearTimeout(timer) } }, [open, productId, items, mounted]) async function loadVariations(nextProductId: string, signal?: AbortSignal) { setIsLoading(true) try { const data = await getProductVariationValues(nextProductId, signal) setVariations(data.variations) } catch (err) { if (err instanceof DOMException && err.name === 'AbortError') return if (err instanceof ApiError) { setError(err.message) } else { setError(t('storeItems.create.errorVariations')) } } finally { setIsLoading(false) } } useEffect(() => { if (!mounted || closing) return const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape' && !isSubmitting) onClose() } document.addEventListener('keydown', onKey) return () => document.removeEventListener('keydown', onKey) }, [mounted, closing, onClose, isSubmitting]) if (!mounted || !productId) return null 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) => { if (prev.length <= 1) return prev const target = prev.find((row) => row.id === rowId) if (target?.storeItemId) { setRemovedIds((ids) => [...ids, target.storeItemId!]) } return prev.filter((row) => row.id !== rowId) }) } const canSubmit = rows.length > 0 && rows.every((row) => { const price = parseIrtInput(row.price) if (price !== null && price < 0) return false if (row.unlimitedStock) return true const stock = Number(row.stock) return row.stock.trim() !== '' && !Number.isNaN(stock) && stock >= 0 }) && !isSubmitting && !isLoading async function handleSubmit(e: React.FormEvent) { e.preventDefault() if (!canSubmit) return setIsSubmitting(true) setError('') try { const data = await syncProductStoreItems({ productId, isUsed, items: rows.map((row) => ({ id: row.storeItemId, selections: formVariations.flatMap((variation) => { const optionId = row.selections[variation.id] if (!optionId) return [] return [{ variationId: variation.id, optionId }] }), price: parseIrtInput(row.price), stockQuantity: row.unlimitedStock ? null : row.stock.trim() ? Number(row.stock) : null, })), removedIds, }) onSaved?.(data.items) onClose() } catch (err) { if (err instanceof ApiError) { setError(err.message) } else { setError(t('storeItems.edit.errorSave')) } } finally { setIsSubmitting(false) } } const subtitle = isFa ? [productNameFa, productTitle].filter(Boolean).join(' / ') : [productTitle, productNameFa].filter(Boolean).join(' / ') return createPortal(
{ if (!isSubmitting) onClose() }} >
e.stopPropagation()} role="dialog" aria-modal="true" aria-labelledby="edit-store-items-title" lang={isFa ? 'fa' : 'en'} dir={isFa ? 'rtl' : 'ltr'} >

{t('storeItems.edit.title')}

{subtitle}

void handleSubmit(e)}> {isLoading ? (

{t('storeItems.create.loadingVariations')}

) : ( <>
{formVariations.map((variation) => ( {variation.name} ))} {t('storeItems.create.price')} {t('storeItems.create.stock')} {t('storeItems.create.unlimited')}
{rows.map((row) => (
{formVariations.map((variation) => { const options = getVariationOptions(variation) return ( ) })} updateRow(row.id, { price: formatIrtInput(e.target.value) })} placeholder={t('storeItems.price.contact')} disabled={isSubmitting} /> updateRow(row.id, { stock: e.target.value })} disabled={isSubmitting || row.unlimitedStock} placeholder={row.unlimitedStock ? '∞' : '0'} />
updateRow(row.id, { unlimitedStock: e.target.checked }) } disabled={isSubmitting} aria-label={t('storeItems.create.unlimited')} />
))}
)} {error &&

{error}

}
, document.body, ) }