Add store-product Excel bulk edit and polish the business dashboard.

Export/import price and stock from Store Products, keep card order after saves, localize variations, and drop unused website nav items.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-19 01:02:43 +03:30
co-authored by Cursor
parent 7b51dda82a
commit 832ac32a09
16 changed files with 617 additions and 181 deletions
+134 -22
View File
@@ -1,12 +1,13 @@
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { Package, Plus, RotateCcw, Search } from 'lucide-react'
import { Download, Loader2, Package, Plus, RotateCcw, Search, Upload } from 'lucide-react'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { CreateStoreItemsModal } from '../components/CreateStoreItemsModal'
import { EditStoreItemsModal } from '../components/EditStoreItemsModal'
import { Pagination } from '../components/Pagination'
import { Tooltip } from '../components/Tooltip'
import { PickStoreVariantModal } from '../components/PickStoreVariantModal'
import { ShoppingCartModal } from '../components/ShoppingCartModal'
import { StoreItemCard } from '../components/StoreItemCard'
@@ -18,6 +19,8 @@ import { useToast } from '../context/ToastContext'
import { ApiError } from '../lib/api'
import {
deleteStoreItemsByProduct,
downloadStoreItemsExcel,
importStoreItemsExcel,
listAllStoreItems,
STORE_ITEMS_PER_PAGE,
type StoreItem,
@@ -75,6 +78,9 @@ function StoreItemsPageContent() {
const [appliedFilters, setAppliedFilters] = useState<StoreListingFilters>(
EMPTY_STORE_LISTING_FILTERS,
)
const [excelBusy, setExcelBusy] = useState<'export' | 'import' | null>(null)
const [productOrder, setProductOrder] = useState<string[]>([])
const importInputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (hasItems) {
@@ -102,7 +108,10 @@ function StoreItemsPageContent() {
}
}, [hasItems, loadShoppingCard, showToast])
const listings = useMemo(() => groupStoreItemsByProduct(items), [items])
const listings = useMemo(
() => groupStoreItemsByProduct(items, productOrder),
[items, productOrder],
)
const filteredListings = useMemo(
() => filterStoreListings(listings, appliedFilters),
[listings, appliedFilters],
@@ -177,6 +186,7 @@ function StoreItemsPageContent() {
try {
const data = await listAllStoreItems(signal)
setItems(data)
setProductOrder(groupStoreItemsByProduct(data).map((listing) => listing.productId))
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return
if (err instanceof ApiError) {
@@ -197,6 +207,10 @@ function StoreItemsPageContent() {
const withoutProduct = prev.filter((item) => item.productId !== productId)
return updated.length > 0 ? [...withoutProduct, ...updated] : withoutProduct
})
if (updated.length === 0) {
setProductOrder((prev) => prev.filter((id) => id !== productId))
}
}
function mergeUpdatedItems(updated: StoreItem[]) {
@@ -244,6 +258,64 @@ function StoreItemsPageContent() {
showToast(t('storeItems.addedToCart'), 'success')
}
async function handleExportExcel() {
if (excelBusy) return
setExcelBusy('export')
setError('')
try {
await downloadStoreItemsExcel()
} catch (err) {
if (err instanceof ApiError) {
setError(err.message)
} else {
setError(t('storeItems.excel.exportError'))
}
} finally {
setExcelBusy(null)
}
}
function openImportPicker() {
if (excelBusy) return
importInputRef.current?.click()
}
async function handleImportExcel(fileList: FileList | null) {
const file = fileList?.[0]
if (importInputRef.current) {
importInputRef.current.value = ''
}
if (!file) return
const name = file.name.toLowerCase()
if (!name.endsWith('.xlsx')) {
setError(t('storeItems.excel.importEmpty'))
return
}
setExcelBusy('import')
setError('')
try {
const result = await importStoreItemsExcel(file)
showToast(
t('storeItems.excel.importSuccess', {
updated: result.updated,
unchanged: result.unchanged,
}),
'success',
)
await loadItems()
} catch (err) {
if (err instanceof ApiError) {
setError(err.message)
} else {
setError(t('storeItems.excel.importError'))
}
} finally {
setExcelBusy(null)
}
}
async function confirmDelete() {
if (!deleteTarget) return
@@ -253,6 +325,7 @@ function StoreItemsPageContent() {
try {
await deleteStoreItemsByProduct(deleteTarget.productId)
setItems((prev) => prev.filter((item) => item.productId !== deleteTarget.productId))
setProductOrder((prev) => prev.filter((id) => id !== deleteTarget.productId))
setDeleteTarget(null)
const nextTotal = Math.max(0, totalListings - 1)
@@ -342,25 +415,64 @@ function StoreItemsPageContent() {
</div>
</div>
<div className={filterStyles.filterActions}>
<button
type="submit"
className={`${filterStyles.iconActionBtn} ${filterStyles.iconActionBtnPrimary}`}
disabled={isLoading}
aria-label={t('storeItems.search')}
title={t('storeItems.search')}
>
<Search size={18} />
</button>
<button
type="button"
className={`${filterStyles.iconActionBtn} ${filterStyles.iconActionBtnGhost}`}
onClick={clearFilters}
disabled={isLoading}
aria-label={t('storeItems.clearFilters')}
title={t('storeItems.clearFilters')}
>
<RotateCcw size={18} />
</button>
<input
ref={importInputRef}
type="file"
accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
hidden
onChange={(e) => void handleImportExcel(e.target.files)}
/>
<Tooltip label={t('storeItems.excel.export')}>
<button
type="button"
className={`${filterStyles.iconActionBtn} ${filterStyles.iconActionBtnGhost}`}
onClick={() => void handleExportExcel()}
disabled={isLoading || !!excelBusy || listings.length === 0}
aria-label={t('storeItems.excel.export')}
>
{excelBusy === 'export' ? (
<Loader2 size={18} className={styles.spin} />
) : (
<Download size={18} />
)}
</button>
</Tooltip>
<Tooltip label={t('storeItems.excel.import')}>
<button
type="button"
className={`${filterStyles.iconActionBtn} ${filterStyles.iconActionBtnGhost}`}
onClick={openImportPicker}
disabled={isLoading || !!excelBusy}
aria-label={t('storeItems.excel.import')}
>
{excelBusy === 'import' ? (
<Loader2 size={18} className={styles.spin} />
) : (
<Upload size={18} />
)}
</button>
</Tooltip>
<Tooltip label={t('storeItems.search')}>
<button
type="submit"
className={`${filterStyles.iconActionBtn} ${filterStyles.iconActionBtnPrimary}`}
disabled={isLoading || !!excelBusy}
aria-label={t('storeItems.search')}
>
<Search size={18} />
</button>
</Tooltip>
<Tooltip label={t('storeItems.clearFilters')}>
<button
type="button"
className={`${filterStyles.iconActionBtn} ${filterStyles.iconActionBtnGhost}`}
onClick={clearFilters}
disabled={isLoading || !!excelBusy}
aria-label={t('storeItems.clearFilters')}
>
<RotateCcw size={18} />
</button>
</Tooltip>
</div>
</form>
</div>