From 5f4b0b4f92e1c4cec6967479f378fa510998b82d Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Tue, 18 Aug 2026 11:26:20 +0330 Subject: [PATCH] Fix store item variant defaults and listing sorting Auto-select first option in variation modals and sort store items by availability, then update time. Co-authored-by: Cursor --- .../src/components/CreateStoreItemsModal.tsx | 28 ++++++ .../src/components/EditStoreItemsModal.tsx | 26 ++++++ .../components/PickStoreSpecialItemsModal.tsx | 25 +++++- .../StoreItemMultiSearchSelect.module.css | 10 +++ .../components/StoreItemMultiSearchSelect.tsx | 20 +++++ .../StoreSpecialCarousel.module.css | 25 ++++++ .../src/components/StoreSpecialCarousel.tsx | 7 +- .../src/components/Tooltip.module.css | 16 ++-- apps/business/src/components/Tooltip.tsx | 85 ++++++++++++++++--- .../business/src/services/storeItemService.ts | 1 + apps/business/src/utils/storeProductGroups.ts | 37 ++++++-- .../src/utils/storeSpecialListings.ts | 1 + .../src/components/Tooltip.module.css | 16 ++-- apps/customer/src/components/Tooltip.tsx | 84 +++++++++++++++--- .../BusinessActionsOverflowMenu.module.css | 5 +- .../BusinessActionsOverflowMenu.tsx | 17 ++-- .../components/DomainLinksOverflowMenu.tsx | 17 ++-- .../PrimaryColorSwatchControl.module.css | 1 - .../components/PrimaryColorSwatchControl.tsx | 17 ++-- apps/super-admin/src/utils/fitFixedPopover.ts | 35 ++++++++ 20 files changed, 409 insertions(+), 64 deletions(-) create mode 100644 apps/super-admin/src/utils/fitFixedPopover.ts diff --git a/apps/business/src/components/CreateStoreItemsModal.tsx b/apps/business/src/components/CreateStoreItemsModal.tsx index e3886aa..6abf0bc 100644 --- a/apps/business/src/components/CreateStoreItemsModal.tsx +++ b/apps/business/src/components/CreateStoreItemsModal.tsx @@ -72,6 +72,34 @@ export function CreateStoreItemsModal({ [variations], ) + // When modal loads/variations change, pre-select the first option for every variation. + // This matches the expected UX and avoids empty selects on initial open. + useEffect(() => { + if (!mounted || closing) return + if (formVariations.length === 0) return + + setRows((prev) => { + // Avoid re-setting state if everything is already selected. + 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'] diff --git a/apps/business/src/components/EditStoreItemsModal.tsx b/apps/business/src/components/EditStoreItemsModal.tsx index ba5ed3f..f03192f 100644 --- a/apps/business/src/components/EditStoreItemsModal.tsx +++ b/apps/business/src/components/EditStoreItemsModal.tsx @@ -83,6 +83,32 @@ export function EditStoreItemsModal({ [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'] diff --git a/apps/business/src/components/PickStoreSpecialItemsModal.tsx b/apps/business/src/components/PickStoreSpecialItemsModal.tsx index b3b74d3..8f635d1 100644 --- a/apps/business/src/components/PickStoreSpecialItemsModal.tsx +++ b/apps/business/src/components/PickStoreSpecialItemsModal.tsx @@ -41,6 +41,7 @@ export function PickStoreSpecialItemsModal({ const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState('') const [selectedIds, setSelectedIds] = useState([]) + const [selectOpen, setSelectOpen] = useState(false) const existingSet = useMemo(() => new Set(existingStoreItemIds), [existingStoreItemIds]) @@ -85,15 +86,28 @@ export function PickStoreSpecialItemsModal({ const items = await listAllStoreItems(signal) const listings = groupStoreItemsByProduct(items) const nextOptions = listings - .map((listing) => { + .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) .filter((option) => !existingSet.has(option.id)) setOptions(nextOptions) @@ -133,7 +147,11 @@ export function PickStoreSpecialItemsModal({ return createPortal(
e.target === e.currentTarget && onClose()} + onClick={(e) => { + if (e.target !== e.currentTarget) return + if (selectOpen) return + onClose() + }} role="presentation" >
)} diff --git a/apps/business/src/components/StoreItemMultiSearchSelect.module.css b/apps/business/src/components/StoreItemMultiSearchSelect.module.css index 04b79b4..c89b044 100644 --- a/apps/business/src/components/StoreItemMultiSearchSelect.module.css +++ b/apps/business/src/components/StoreItemMultiSearchSelect.module.css @@ -107,6 +107,16 @@ color: var(--text-secondary); } +.optionPrice { + font-size: 12px; + font-weight: 500; + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: start; +} + .selectedList { display: flex; flex-direction: column; diff --git a/apps/business/src/components/StoreItemMultiSearchSelect.tsx b/apps/business/src/components/StoreItemMultiSearchSelect.tsx index 1b8a468..8248d35 100644 --- a/apps/business/src/components/StoreItemMultiSearchSelect.tsx +++ b/apps/business/src/components/StoreItemMultiSearchSelect.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { Search, X } from 'lucide-react' import { useLocale } from '@meshkee/dashboard-ui' import { useT } from '../i18n/useT' +import { formatIrtPrice } from '../utils/irtPrice' import selectStyles from './SearchableSelect.module.css' import styles from './StoreItemMultiSearchSelect.module.css' @@ -10,6 +11,11 @@ export interface StoreItemSearchOption { title: string nameFa: string image: string | null + /** + * Optional because this shared component is also used for brands/categories. + * When provided, we render it as a single price line. + */ + price?: number | null } interface StoreItemMultiSearchSelectProps { @@ -19,6 +25,11 @@ interface StoreItemMultiSearchSelectProps { placeholder?: string disabled?: boolean minSearchLength?: number + /** + * Optional hook for parents that need to know when the dropdown is open + * (e.g. to prevent modal outside-click closing). + */ + onOpenChange?: (open: boolean) => void } export function StoreItemMultiSearchSelect({ @@ -28,6 +39,7 @@ export function StoreItemMultiSearchSelect({ placeholder, disabled = false, minSearchLength = 3, + onOpenChange, }: StoreItemMultiSearchSelectProps) { const t = useT() const { locale } = useLocale() @@ -40,6 +52,10 @@ export function StoreItemMultiSearchSelect({ const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]) const searchPlaceholder = placeholder ?? t('storeItems.search.minChars', { count: minSearchLength }) + useEffect(() => { + onOpenChange?.(open) + }, [open, onOpenChange]) + const filtered = useMemo(() => { const trimmed = query.trim() if (trimmed.length < minSearchLength) return [] @@ -79,6 +95,9 @@ export function StoreItemMultiSearchSelect({ : '' : option.nameFa + const priceLine = + option.price === undefined ? null : option.price === null ? ' - ' : formatIrtPrice(option.price) + return ( ) : null} + {priceLine ? {priceLine} : null} ) } diff --git a/apps/business/src/components/StoreSpecialCarousel.module.css b/apps/business/src/components/StoreSpecialCarousel.module.css index 6e8749e..a28b47e 100644 --- a/apps/business/src/components/StoreSpecialCarousel.module.css +++ b/apps/business/src/components/StoreSpecialCarousel.module.css @@ -16,6 +16,31 @@ color: var(--text-primary); } +.titleRow { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.keyChip { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px 10px; + font-size: 12px; + font-weight: 600; + color: var(--primary); + background: rgba(var(--primary-rgb) / 0.08); + border: 1px solid rgba(var(--primary-rgb) / 0.18); + border-radius: var(--radius-sm); + user-select: text; + white-space: nowrap; + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; +} + .headerActions { display: flex; align-items: center; diff --git a/apps/business/src/components/StoreSpecialCarousel.tsx b/apps/business/src/components/StoreSpecialCarousel.tsx index fb41b6b..d003adb 100644 --- a/apps/business/src/components/StoreSpecialCarousel.tsx +++ b/apps/business/src/components/StoreSpecialCarousel.tsx @@ -83,7 +83,12 @@ export function StoreSpecialCarousel({ return (
-

{special.title}

+
+ + {special.key} + +

{special.title}

+