"use client"; import Image from "next/image"; import { FormEvent, useCallback, useEffect, useMemo, useState } from "react"; import ClothesShell from "@/components/clothes/ClothesShell"; import IconButton, { ClearFilterIcon, DiscountIcon, EditIcon, FilterIcon, TrashIcon, VariantIcon, } from "@/components/ui/IconButton"; import Modal, { FormModal } from "@/components/ui/Modal"; import Pagination, { usePagination } from "@/components/ui/Pagination"; import { Brand, CategoryNode, discountPercent, findCategoryNode, flattenCategories, formatPrice, getCategoryVariantGroups, getVariantLabel, Product, ProductVariantPrice, } from "@/data/clothes"; import { deleteProduct, fetchBrands, fetchCategoriesTree, fetchProducts, toVariantPayload, updateProduct, } from "@/lib/clothes-api"; import { ApiError } from "@/lib/api"; import { useConfirm } from "@/components/ui/ConfirmDialog"; const PAGE_SIZE = 4; type Filters = { name: string; categoryId: string; priceMin: string; priceMax: string; }; const emptyFilters: Filters = { name: "", categoryId: "", priceMin: "", priceMax: "", }; function emptyVariantRow(price = ""): ProductVariantPrice { return { id: `vp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, colorId: "", sizeId: "", packageId: "", price, discountPrice: "", }; } function isBaseRow(row: ProductVariantPrice) { return !row.colorId && !row.sizeId && !row.packageId; } export default function ProductListPage() { const confirmDialog = useConfirm(); const [categoryTree, setCategoryTree] = useState([]); const [brands, setBrands] = useState([]); const categoryOptions = useMemo( () => flattenCategories(categoryTree), [categoryTree], ); const brandById = useMemo( () => new Map(brands.map((b) => [b.id, b])), [brands], ); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [draft, setDraft] = useState(emptyFilters); const [applied, setApplied] = useState(emptyFilters); const [detailProduct, setDetailProduct] = useState(null); const [variantProduct, setVariantProduct] = useState(null); const [variantRows, setVariantRows] = useState([ emptyVariantRow(), ]); const [discountProduct, setDiscountProduct] = useState(null); const [discountRows, setDiscountRows] = useState([]); const reload = useCallback(async () => { setLoading(true); setLoadError(null); try { const [products, cats, brandRows] = await Promise.all([ fetchProducts({ name: applied.name.trim() || undefined, categoryId: applied.categoryId || undefined, minPrice: applied.priceMin ? Number(applied.priceMin) : undefined, maxPrice: applied.priceMax ? Number(applied.priceMax) : undefined, }), fetchCategoriesTree(), fetchBrands(), ]); setItems(products); setCategoryTree(cats); setBrands(brandRows); } catch (err) { setLoadError( err instanceof ApiError ? err.message : "بارگذاری محصولات ناموفق بود. آیا API روشن است؟", ); } finally { setLoading(false); } }, [applied]); useEffect(() => { void reload(); }, [reload]); const list = items; const { page, setPage, pageItems, totalPages, totalItems, pageSize } = usePagination(list, PAGE_SIZE, JSON.stringify(applied)); const variantGroups = useMemo( () => variantProduct ? getCategoryVariantGroups(variantProduct.categoryId, categoryTree) : { colors: [], sizes: [], packages: [] }, [variantProduct, categoryTree], ); const hasAnyVariantOption = variantGroups.colors.length > 0 || variantGroups.sizes.length > 0 || variantGroups.packages.length > 0; function openVariants(product: Product) { setVariantProduct(product); setVariantRows( product.variantPrices?.length ? product.variantPrices.map((row) => ({ ...row })) : [emptyVariantRow(String(product.price))], ); } function closeVariants() { setVariantProduct(null); } function openDiscount(product: Product) { setDiscountProduct(product); if (product.variantPrices?.length) { setDiscountRows(product.variantPrices.map((row) => ({ ...row }))); } else { setDiscountRows([ { ...emptyVariantRow(String(product.price)), discountPrice: typeof product.discountPrice === "number" ? String(product.discountPrice) : "", }, ]); } } function closeDiscount() { setDiscountProduct(null); } function updateRow( id: string, field: keyof Omit, value: string, ) { setVariantRows((prev) => prev.map((row) => (row.id === id ? { ...row, [field]: value } : row)), ); } function updateDiscountPrice(id: string, value: string) { setDiscountRows((prev) => prev.map((row) => row.id === id ? { ...row, discountPrice: value } : row, ), ); } function addRow() { setVariantRows((prev) => [ ...prev, emptyVariantRow(variantProduct ? String(variantProduct.price) : ""), ]); } function removeRow(id: string) { setVariantRows((prev) => prev.length <= 1 ? prev : prev.filter((row) => row.id !== id), ); } async function saveVariants(event: FormEvent) { event.preventDefault(); if (!variantProduct) return; try { const updated = await updateProduct(variantProduct.id, { variants: toVariantPayload(variantRows), }); setItems((prev) => prev.map((product) => product.id === updated.id ? updated : product, ), ); closeVariants(); } catch (err) { alert(err instanceof ApiError ? err.message : "ذخیره تنوع ناموفق بود"); } } async function saveDiscount(event: FormEvent) { event.preventDefault(); if (!discountProduct) return; const nextRows = discountRows.map((row) => ({ ...row })); const discounted = nextRows .map((row) => Number(row.discountPrice)) .filter((n) => !Number.isNaN(n) && n > 0); const hadVariants = Boolean(discountProduct.variantPrices?.length); const baseDiscount = nextRows.find((row) => isBaseRow(row))?.discountPrice; const nextProductDiscount = baseDiscount ? Number(baseDiscount) || null : discounted.length ? Math.min(...discounted) : null; try { const updated = await updateProduct(discountProduct.id, { discountPrice: nextProductDiscount, ...(hadVariants ? { variants: toVariantPayload(nextRows) } : {}), }); setItems((prev) => prev.map((product) => product.id === updated.id ? updated : product, ), ); closeDiscount(); } catch (err) { alert(err instanceof ApiError ? err.message : "ذخیره تخفیف ناموفق بود"); } } async function handleDelete(productId: string) { if (!(await confirmDialog("این محصول حذف شود؟"))) return; try { await deleteProduct(productId); setItems((prev) => prev.filter((p) => p.id !== productId)); } catch (err) { alert(err instanceof ApiError ? err.message : "حذف ناموفق بود"); } } return ( <>
setDraft((prev) => ({ ...prev, name: e.target.value })) } placeholder="جستجو..." />
setDraft((prev) => ({ ...prev, priceMin: e.target.value })) } placeholder="1000000" />
setDraft((prev) => ({ ...prev, priceMax: e.target.value })) } placeholder="5000000" />
setApplied(draft)} > { setDraft(emptyFilters); setApplied(emptyFilters); }} >
{loadError ? (

{loadError}

) : null} {loading ? (

در حال بارگذاری محصولات…

) : null}
{!loading && pageItems.length === 0 ? (

محصولی یافت نشد.

) : null} {pageItems.map((product, i) => { const hasDiscount = typeof product.discountPrice === "number" && product.discountPrice < product.price; const percent = hasDiscount ? discountPercent(product.price, product.discountPrice!) : null; const variantCount = product.variantPrices?.length ?? 0; const discountCount = product.variantPrices?.length ? product.variantPrices.filter((row) => { const n = Number(row.discountPrice); return row.discountPrice && !Number.isNaN(n) && n > 0; }).length : hasDiscount ? 1 : 0; return (
setDetailProduct(product)} >
{product.nameFa} {percent ? ( {percent}%- ) : null}

{product.nameFa}

{product.nameEn}

{hasDiscount ? ( <> {formatPrice(product.discountPrice!)} تومان {formatPrice(product.price)} تومان ) : ( {formatPrice(product.price)} تومان )}
e.stopPropagation()} > void handleDelete(product.id)} > openVariants(product)} > openDiscount(product)} >
); })}
{list.length === 0 ? (

محصولی با این فیلترها پیدا نشد.

) : ( )}
setDetailProduct(null)} wide="full" footer={ } > {detailProduct ? (
{detailProduct.nameFa}
{(detailProduct.gallery?.length ?? 0) > 0 ? (

گالری

{detailProduct.gallery!.map((src) => (
))}
) : null}

{detailProduct.nameFa}

{detailProduct.nameEn ? (

{detailProduct.nameEn}

) : null}
دسته‌بندی
{findCategoryNode(categoryTree, detailProduct.categoryId) ?.nameFa || "—"}
برند
{brandById.get(detailProduct.brandId)?.nameFa || "—"}
قیمت
{typeof detailProduct.discountPrice === "number" && detailProduct.discountPrice < detailProduct.price ? ( <> {formatPrice(detailProduct.discountPrice)} تومان {formatPrice(detailProduct.price)} تومان ) : ( {formatPrice(detailProduct.price)} تومان )}
{detailProduct.summary ? (

خلاصه

{detailProduct.summary}

) : null} {detailProduct.description ? (

توضیحات

{detailProduct.description}

) : null} {detailProduct.tags.length ? (

برچسب‌ها

{detailProduct.tags.map((tag) => ( {tag} ))}
) : null}

تنوع‌ها

{detailProduct.variantPrices?.length ? (
    {detailProduct.variantPrices.map((row) => { const rowDiscount = Number(row.discountPrice); const hasRowDiscount = row.discountPrice && !Number.isNaN(rowDiscount) && rowDiscount > 0; return (
  • {getVariantLabel( row, detailProduct.categoryId, categoryTree, )} {hasRowDiscount ? ( <> {formatPrice(rowDiscount)} {formatPrice(Number(row.price) || 0)} ) : ( formatPrice(Number(row.price) || 0) )}
  • ); })}
) : (

تنوعی ثبت نشده است.

)}
) : null}

هر نوع تنوع (رنگ، سایز، پکیج) جداست. می‌توانید خالی بگذارید.

ردیف‌ها
{variantRows.map((row, index) => (
ردیف {new Intl.NumberFormat("en-US").format(index + 1)}
{variantGroups.colors.length ? (
) : null} {variantGroups.sizes.length ? (
) : null} {variantGroups.packages.length ? (
) : null} {!hasAnyVariantOption ? (

تنوعی برای این دسته تعریف نشده.

) : null}
updateRow(row.id, "price", e.target.value) } placeholder="3570000" />
))}

تنوع‌های موجود و قیمت فعلی را ببینید و قیمت تخفیف را وارد کنید.

{discountRows.map((row) => { const currentPrice = Number(row.price); const label = discountProduct ? getVariantLabel(row, discountProduct.categoryId, categoryTree) : "بدون تنوع"; return (
{label}
قیمت فعلی

{Number.isNaN(currentPrice) ? row.price || "—" : formatPrice(currentPrice)} {!Number.isNaN(currentPrice) && row.price ? ( تومان ) : null}

updateDiscountPrice(row.id, e.target.value) } placeholder="3570000" />
); })}
); }