import { useEffect, useRef, useState } from 'react' import { Link, useNavigate, useParams } from 'react-router-dom' import { Check, ClipboardList, Images, SlidersHorizontal } from 'lucide-react' import { type CityOption, } from '@meshkee/dashboard-ui' import { Breadcrumbs } from '../components/Breadcrumbs' import { useToast } from '../context/ToastContext' import pageStyles from '../components/PageContent.module.css' import { CitySearchSelect } from '../components/CitySearchSelect' import { CategorySearchSelect } from '../components/CategorySearchSelect' import { ImageCropper } from '../components/ImageCropper' import { ImageUploader } from '../components/ImageUploader' import { SeoMetaFields } from '../components/SeoMetaFields' import { useT } from '../i18n/useT' import { translate } from '../i18n/messages' import { ApiError, isAbortError } from '../lib/api' import { listCitiesByCountrySlug, listCountries, } from '../services/citiesService' import { buildTechnicalValuesPayload, createUserProduct, getUserProduct, getUserProductCategoryTechnicalForm, listUserProductCategories, updateUserProduct, type TechnicalFormField, type TechnicalFormValues, type UserProductCategoryOption, type UserProductCondition, type UserProductPriceCurrency, type UserProductTechnicalValueInput, } from '../services/userProductsService' import { galleryMediaIdsAfterChange, resolveDataUrlToMediaId, resolveDataUrlsToMediaIds } from '../services/mediaService' import { formatIrtInput, parseIrtInput } from '../utils/irtPrice' import { seoMetaPayload } from '../utils/seoMeta' import styles from './AddCustomerProductPage.module.css' type StepId = 1 | 2 | 3 const PRICE_UNITS: UserProductPriceCurrency[] = ['IRT', 'USD', 'EUR', 'AED'] const CONDITIONS: UserProductCondition[] = [ 'new', 'stock', 'needs_repair', 'scrap', ] function fieldLabel(field: TechnicalFormField) { return field.label } function optionLabel(option: TechnicalFormField['options'][number]) { return option.label } function mapTechnicalValues( items: UserProductTechnicalValueInput[], ): TechnicalFormValues { const values: TechnicalFormValues = {} for (const item of items) { if (item.textValue != null) { values[item.fieldId] = item.textValue continue } if (item.optionId) { values[item.fieldId] = item.optionId continue } if (item.optionIds?.length) { values[item.fieldId] = item.optionIds } } return values } function isCondition(value: string | null | undefined): value is UserProductCondition { return ( value === 'new' || value === 'stock' || value === 'needs_repair' || value === 'scrap' ) } function isPriceCurrency( value: string | null | undefined, ): value is UserProductPriceCurrency { return ( value === 'IRT' || value === 'USD' || value === 'EUR' || value === 'AED' ) } export function AddCustomerProductPage() { const t = useT() const navigate = useNavigate() const { id } = useParams<{ id?: string }>() const isEdit = Boolean(id) const { showToast } = useToast() const [loaded, setLoaded] = useState(!isEdit) const [step, setStep] = useState(1) const [stepError, setStepError] = useState('') const [submitting, setSubmitting] = useState(false) const pendingTechnicalValuesRef = useRef(null) const [categories, setCategories] = useState([]) const [loadingCategories, setLoadingCategories] = useState(false) const [categoryId, setCategoryId] = useState('') const [titleFa, setTitleFa] = useState('') const [titleEn, setTitleEn] = useState('') const [description, setDescription] = useState('') const [seoMetaTitle, setSeoMetaTitle] = useState('') const [seoMetaDescription, setSeoMetaDescription] = useState('') const [priceInput, setPriceInput] = useState('') const [priceUnit, setPriceUnit] = useState('IRT') const [priceByExpert, setPriceByExpert] = useState(false) const [countries, setCountries] = useState([]) const [cities, setCities] = useState([]) const [countryId, setCountryId] = useState('') const [cityId, setCityId] = useState('') const [deliveryNote, setDeliveryNote] = useState('') const [loadingLocations, setLoadingLocations] = useState(false) const [thumbnail, setThumbnail] = useState(null) const [featuredMediaId, setFeaturedMediaId] = useState(null) const [gallery, setGallery] = useState([]) const [galleryMediaIds, setGalleryMediaIds] = useState([]) const [condition, setCondition] = useState('new') const [technicalNotes, setTechnicalNotes] = useState('') const [technicalFields, setTechnicalFields] = useState([]) const [technicalValues, setTechnicalValues] = useState({}) const [loadingTechnicalForm, setLoadingTechnicalForm] = useState(false) const steps: { id: StepId; label: string }[] = [ { id: 1, label: t('customerProducts.form.step.basics') }, { id: 2, label: t('customerProducts.form.step.images') }, { id: 3, label: t('customerProducts.form.step.technical') }, ] const stepIcon = step === 1 ? ( ) : step === 2 ? ( ) : ( ) const stepLabelKey = step === 1 ? 'customerProducts.form.step.basics' : step === 2 ? 'customerProducts.form.step.images' : 'customerProducts.form.step.technical' const stepNameFa = translate('fa', stepLabelKey) const stepNameEn = translate('en', stepLabelKey) useEffect(() => { const controller = new AbortController() setLoadingLocations(true) void listCountries(controller.signal) .then((items) => { if (!controller.signal.aborted) setCountries(items) }) .catch((err) => { if (!isAbortError(err)) setStepError(t('customerProducts.form.error.loadLocations')) }) .finally(() => { if (!controller.signal.aborted) setLoadingLocations(false) }) return () => controller.abort() }, [t]) useEffect(() => { const controller = new AbortController() setLoadingCategories(true) void listUserProductCategories(controller.signal) .then((response) => { if (!controller.signal.aborted) setCategories(response.items) }) .catch((err) => { if (!isAbortError(err)) setStepError(t('customerProducts.form.error.loadCategories')) }) .finally(() => { if (!controller.signal.aborted) setLoadingCategories(false) }) return () => controller.abort() }, [t]) useEffect(() => { if (!isEdit || !id) return const controller = new AbortController() async function loadProduct() { setStepError('') try { const response = await getUserProduct(id!, controller.signal) if (controller.signal.aborted) return const product = response.product pendingTechnicalValuesRef.current = mapTechnicalValues( product.technicalValues ?? [], ) setCategoryId(product.categoryId ?? '') setTitleFa(product.titleFa ?? product.title ?? '') setTitleEn(product.titleEn ?? '') setDescription(product.description ?? '') setSeoMetaTitle(product.seoMetaTitle ?? '') setSeoMetaDescription(product.seoMetaDescription ?? '') setPriceInput( product.price != null ? formatIrtInput(String(product.price)) : '', ) setPriceUnit( isPriceCurrency(product.priceCurrency) ? product.priceCurrency : 'IRT', ) setPriceByExpert(product.priceByExpert === true) setCountryId(product.countryId) setCityId(product.cityId) setDeliveryNote(product.deliveryNote ?? '') setThumbnail(product.imageUrl) setFeaturedMediaId(product.featuredMediaId) setGallery((product.images ?? []).map((item) => item.url)) setGalleryMediaIds(product.galleryMediaIds ?? []) setCondition(isCondition(product.condition) ? product.condition : 'new') setTechnicalNotes(product.technicalNotes ?? '') if (product.countrySlug) { setCities(await listCitiesByCountrySlug(product.countrySlug)) } if (!controller.signal.aborted) setLoaded(true) } catch (err) { if (isAbortError(err) || controller.signal.aborted) return showToast( err instanceof ApiError ? err.message : t('customerProducts.form.error.load'), 'error', ) navigate('/customer-products', { replace: true }) } } void loadProduct() return () => controller.abort() }, [isEdit, id, navigate, showToast, t]) useEffect(() => { if (!pendingTechnicalValuesRef.current) { setTechnicalValues({}) } setTechnicalFields([]) if (!categoryId) return const controller = new AbortController() setLoadingTechnicalForm(true) void getUserProductCategoryTechnicalForm(categoryId, controller.signal) .then((response) => { if (controller.signal.aborted) return setTechnicalFields(response.form?.fields ?? []) if (pendingTechnicalValuesRef.current) { setTechnicalValues(pendingTechnicalValuesRef.current) pendingTechnicalValuesRef.current = null } }) .catch((err) => { if (!isAbortError(err)) { setTechnicalFields([]) setStepError(t('customerProducts.form.error.loadTechnicalForm')) } }) .finally(() => { if (!controller.signal.aborted) setLoadingTechnicalForm(false) }) return () => controller.abort() }, [categoryId, t]) async function handleCountryChange(nextCountryId: string) { setCountryId(nextCountryId) const country = countries.find((item) => item.id === nextCountryId) const nextSlug = country?.slug ?? '' setCityId('') setCities([]) if (!nextSlug) return setLoadingLocations(true) try { setCities(await listCitiesByCountrySlug(nextSlug)) } catch { setStepError(t('customerProducts.form.error.loadLocations')) } finally { setLoadingLocations(false) } } function validateStep1() { if (!categoryId) return t('customerProducts.form.error.categoryRequired') if (!titleFa.trim()) return t('customerProducts.form.error.titleFaRequired') if (!countryId || !cityId) { return t('customerProducts.form.error.locationRequired') } const price = parseIrtInput(priceInput) if (priceInput.trim() && (price === null || price < 0)) { return t('customerProducts.form.error.priceInvalid') } return '' } function validateStep3() { if (!condition) return t('customerProducts.form.error.conditionRequired') for (const field of technicalFields) { if (!field.isRequired) continue const value = technicalValues[field.id] const ok = field.type === 'multi_select' ? Array.isArray(value) && value.length > 0 : typeof value === 'string' && value.trim().length > 0 if (!ok) { return t('customerProducts.form.error.technicalRequired') } } return '' } function goNext() { setStepError('') if (step === 1) { const error = validateStep1() if (error) { setStepError(error) return } setStep(2) return } if (step === 2) setStep(3) } function goBack() { setStepError('') if (step === 2) setStep(1) if (step === 3) setStep(2) } async function handleSubmit() { setStepError('') const validationError = validateStep3() if (validationError) { setStepError(validationError) return } const price = parseIrtInput(priceInput) setSubmitting(true) try { let nextFeaturedMediaId = featuredMediaId if (thumbnail?.startsWith('data:')) { nextFeaturedMediaId = await resolveDataUrlToMediaId( thumbnail, 'user-product-thumbnail.jpg', featuredMediaId, ) } else if (!thumbnail) { nextFeaturedMediaId = null } const nextGalleryMediaIds = await resolveDataUrlsToMediaIds( gallery, galleryMediaIds, ) const payload = { titleFa: titleFa.trim(), titleEn: titleEn.trim() || undefined, description: description.trim() || undefined, ...seoMetaPayload(seoMetaTitle, seoMetaDescription), categoryId, price: price ?? undefined, priceCurrency: priceUnit, priceByExpert, countryId, cityId, deliveryNote: deliveryNote.trim() || undefined, condition, technicalNotes: technicalNotes.trim() || undefined, technicalValues: buildTechnicalValuesPayload(technicalFields, technicalValues), featuredMediaId: nextFeaturedMediaId || undefined, galleryMediaIds: nextGalleryMediaIds, } if (isEdit && id) { await updateUserProduct(id, payload) showToast(t('customerProducts.form.updateSuccess'), 'success') } else { await createUserProduct(payload) showToast(t('customerProducts.form.submitSuccess'), 'success') } navigate('/customer-products') } catch (err) { setStepError( err instanceof ApiError ? err.message : isEdit ? t('customerProducts.form.error.update') : t('customerProducts.form.error.submit'), ) } finally { setSubmitting(false) } } function setTechnicalField(fieldId: string, value: string | string[]) { setTechnicalValues((prev) => ({ ...prev, [fieldId]: value })) } function toggleMultiOption(fieldId: string, optionId: string) { setTechnicalValues((prev) => { const current = prev[fieldId] const selected = Array.isArray(current) ? current : [] const next = selected.includes(optionId) ? selected.filter((item) => item !== optionId) : [...selected, optionId] return { ...prev, [fieldId]: next } }) } function renderTechnicalField(field: TechnicalFormField) { const value = technicalValues[field.id] const id = `tech-${field.id}` if (field.type === 'textarea') { return (