mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
RTL carousels/FABs, special key+title fields, slider gallery fixes, and dashboard SSL sync agent for super-admin. Co-authored-by: Cursor <cursoragent@cursor.com>
469 lines
15 KiB
TypeScript
469 lines
15 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
|
import { Plus } from 'lucide-react'
|
|
import { Breadcrumbs } from '../components/Breadcrumbs'
|
|
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
|
|
import { EditStoreItemsModal } from '../components/EditStoreItemsModal'
|
|
import { PickStoreSpecialItemsModal } from '../components/PickStoreSpecialItemsModal'
|
|
import { PickStoreVariantModal } from '../components/PickStoreVariantModal'
|
|
import { ShoppingCartModal } from '../components/ShoppingCartModal'
|
|
import { StoreItemDiscountModal } from '../components/StoreItemDiscountModal'
|
|
import { StoreItemFestivalModal } from '../components/StoreItemFestivalModal'
|
|
import { StoreSpecialCarousel } from '../components/StoreSpecialCarousel'
|
|
import { StoreSpecialModal } from '../components/StoreSpecialModal'
|
|
import { useDraftCart } from '../context/DraftCartContext'
|
|
import { useToast } from '../context/ToastContext'
|
|
import { useT } from '../i18n/useT'
|
|
import { ApiError } from '../lib/api'
|
|
import {
|
|
createStoreSpecial,
|
|
deleteStoreSpecial,
|
|
listStoreSpecials,
|
|
updateStoreSpecial,
|
|
} from '../services/storeSpecialService'
|
|
import type { StoreItem } from '../services/storeItemService'
|
|
import type { StoreSpecial } from '../types/storeSpecial'
|
|
import type { StoreProductListing } from '../utils/storeProductGroups'
|
|
import pageStyles from '../components/PageContent.module.css'
|
|
import fabStyles from './StoreItemsPage.module.css'
|
|
import styles from './StoreSpecialsPage.module.css'
|
|
|
|
export function StoreSpecialsPage() {
|
|
const t = useT()
|
|
const { itemCount, hasItems, addVariant } = useDraftCart()
|
|
const { showToast } = useToast()
|
|
const [specials, setSpecials] = useState<StoreSpecial[]>([])
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [isSaving, setIsSaving] = useState(false)
|
|
const [error, setError] = useState('')
|
|
|
|
const [createOpen, setCreateOpen] = useState(false)
|
|
const [editSpecialTarget, setEditSpecialTarget] = useState<StoreSpecial | null>(null)
|
|
const [deleteSpecialTarget, setDeleteSpecialTarget] = useState<StoreSpecial | null>(null)
|
|
const [pickItemsTarget, setPickItemsTarget] = useState<StoreSpecial | null>(null)
|
|
|
|
const [editListingTarget, setEditListingTarget] = useState<StoreProductListing | null>(null)
|
|
const [discountTarget, setDiscountTarget] = useState<StoreProductListing | null>(null)
|
|
const [festivalTarget, setFestivalTarget] = useState<StoreProductListing | null>(null)
|
|
const [removeTarget, setRemoveTarget] = useState<{
|
|
special: StoreSpecial
|
|
listing: StoreProductListing
|
|
} | null>(null)
|
|
const [pickVariantTarget, setPickVariantTarget] = useState<StoreProductListing | null>(null)
|
|
const [cartOpen, setCartOpen] = useState(false)
|
|
|
|
const productItems = useMemo(() => {
|
|
const target = editListingTarget ?? discountTarget ?? festivalTarget
|
|
if (!target) return []
|
|
return target.variants
|
|
}, [editListingTarget, discountTarget, festivalTarget])
|
|
|
|
useEffect(() => {
|
|
const controller = new AbortController()
|
|
void loadSpecials(controller.signal)
|
|
return () => controller.abort()
|
|
}, [])
|
|
|
|
async function loadSpecials(signal?: AbortSignal) {
|
|
setIsLoading(true)
|
|
setError('')
|
|
|
|
try {
|
|
const data = await listStoreSpecials(1, 50, signal)
|
|
setSpecials(data.items)
|
|
} catch (err) {
|
|
if (err instanceof DOMException && err.name === 'AbortError') return
|
|
if (err instanceof ApiError) {
|
|
setError(err.message)
|
|
} else {
|
|
setError(t('website.specialItems.errorLoad'))
|
|
}
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
function replaceSpecial(updated: StoreSpecial) {
|
|
setSpecials((prev) => prev.map((entry) => (entry.id === updated.id ? updated : entry)))
|
|
}
|
|
|
|
function mergeUpdatedItems(updated: StoreItem[]) {
|
|
const byId = new Map(updated.map((item) => [item.id, item]))
|
|
setSpecials((prev) =>
|
|
prev.map((special) => ({
|
|
...special,
|
|
items: special.items.map((storeItem) => ({
|
|
...storeItem,
|
|
variants: storeItem.variants.map((variant) => {
|
|
const next = byId.get(variant.id)
|
|
if (!next) return variant
|
|
return {
|
|
...variant,
|
|
price: next.price,
|
|
discountedPrice: next.discountedPrice,
|
|
stockQuantity: next.stockQuantity,
|
|
rewardPoints: next.rewardPoints,
|
|
isFestival: next.isFestival,
|
|
}
|
|
}),
|
|
})),
|
|
})),
|
|
)
|
|
}
|
|
|
|
function replaceProductItems(updated: StoreItem[]) {
|
|
const productId = updated[0]?.productId ?? editListingTarget?.productId
|
|
if (!productId) return
|
|
|
|
setSpecials((prev) =>
|
|
prev.map((special) => ({
|
|
...special,
|
|
items: special.items.map((storeItem) => {
|
|
if (storeItem.productId !== productId) return storeItem
|
|
if (updated.length === 0) return storeItem
|
|
|
|
return {
|
|
...storeItem,
|
|
productTitle: updated[0].productTitle,
|
|
productNameFa: updated[0].productNameFa,
|
|
productImage: updated[0].productImage,
|
|
variants: updated.map((item) => ({
|
|
id: item.id,
|
|
label: item.label,
|
|
selections: item.selections,
|
|
price: item.price,
|
|
discountedPrice: item.discountedPrice,
|
|
stockQuantity: item.stockQuantity,
|
|
rewardPoints: item.rewardPoints,
|
|
isFestival: item.isFestival,
|
|
sortOrder: item.sortOrder,
|
|
})),
|
|
}
|
|
}),
|
|
})),
|
|
)
|
|
}
|
|
|
|
async function handleCreateSpecial(values: { key: string; title: string }) {
|
|
setIsSaving(true)
|
|
setError('')
|
|
|
|
try {
|
|
const result = await createStoreSpecial({
|
|
key: values.key,
|
|
title: values.title,
|
|
sortOrder: specials.length,
|
|
})
|
|
setSpecials((prev) => [...prev, result.special])
|
|
setCreateOpen(false)
|
|
showToast(t('website.specialItems.toastCreated'), 'success')
|
|
} catch (err) {
|
|
if (err instanceof ApiError) {
|
|
setError(err.message)
|
|
} else {
|
|
setError(t('website.specialItems.errorCreate'))
|
|
}
|
|
} finally {
|
|
setIsSaving(false)
|
|
}
|
|
}
|
|
|
|
async function handleEditSpecial(values: { key: string; title: string }) {
|
|
if (!editSpecialTarget) return
|
|
|
|
setIsSaving(true)
|
|
setError('')
|
|
|
|
try {
|
|
const result = await updateStoreSpecial(editSpecialTarget.id, {
|
|
key: values.key,
|
|
title: values.title,
|
|
})
|
|
replaceSpecial(result.special)
|
|
setEditSpecialTarget(null)
|
|
showToast(t('website.specialItems.toastUpdated'), 'success')
|
|
} catch (err) {
|
|
if (err instanceof ApiError) {
|
|
setError(err.message)
|
|
} else {
|
|
setError(t('website.specialItems.errorUpdate'))
|
|
}
|
|
} finally {
|
|
setIsSaving(false)
|
|
}
|
|
}
|
|
|
|
async function confirmDeleteSpecial() {
|
|
if (!deleteSpecialTarget) return
|
|
|
|
setIsSaving(true)
|
|
setError('')
|
|
|
|
try {
|
|
await deleteStoreSpecial(deleteSpecialTarget.id)
|
|
setSpecials((prev) => prev.filter((entry) => entry.id !== deleteSpecialTarget.id))
|
|
setDeleteSpecialTarget(null)
|
|
showToast(t('website.specialItems.toastDeleted'), 'success')
|
|
} catch (err) {
|
|
if (err instanceof ApiError) {
|
|
setError(err.message)
|
|
} else {
|
|
setError(t('website.specialItems.errorDelete'))
|
|
}
|
|
} finally {
|
|
setIsSaving(false)
|
|
}
|
|
}
|
|
|
|
async function handleAddItems(storeItemIds: string[]) {
|
|
if (!pickItemsTarget) return
|
|
|
|
const existingIds = pickItemsTarget.items.map((item) => item.id)
|
|
const nextIds = [...existingIds, ...storeItemIds]
|
|
|
|
setIsSaving(true)
|
|
setError('')
|
|
|
|
try {
|
|
const result = await updateStoreSpecial(pickItemsTarget.id, {
|
|
storeItemIds: nextIds,
|
|
})
|
|
replaceSpecial(result.special)
|
|
setPickItemsTarget(null)
|
|
showToast(t('website.specialItems.toastAdded'), 'success')
|
|
} catch (err) {
|
|
if (err instanceof ApiError) {
|
|
setError(err.message)
|
|
} else {
|
|
setError(t('website.specialItems.errorAdd'))
|
|
}
|
|
} finally {
|
|
setIsSaving(false)
|
|
}
|
|
}
|
|
|
|
async function confirmRemoveFromSpecial() {
|
|
if (!removeTarget) return
|
|
|
|
const { special, listing } = removeTarget
|
|
const storeItemId = listing.representative.storeItemId ?? listing.representative.id
|
|
const nextIds = special.items
|
|
.map((item) => item.id)
|
|
.filter((id) => id !== storeItemId)
|
|
|
|
setIsSaving(true)
|
|
setError('')
|
|
|
|
try {
|
|
const result = await updateStoreSpecial(special.id, {
|
|
storeItemIds: nextIds,
|
|
})
|
|
replaceSpecial(result.special)
|
|
setRemoveTarget(null)
|
|
showToast(t('website.specialItems.toastRemoved'), 'success')
|
|
} catch (err) {
|
|
if (err instanceof ApiError) {
|
|
setError(err.message)
|
|
} else {
|
|
setError(t('website.specialItems.errorRemove'))
|
|
}
|
|
} finally {
|
|
setIsSaving(false)
|
|
}
|
|
}
|
|
|
|
function handleAddToCart(listing: StoreProductListing) {
|
|
if (listing.variantCount > 1) {
|
|
setPickVariantTarget(listing)
|
|
return
|
|
}
|
|
|
|
const variant = listing.variants[0]
|
|
if (!variant) return
|
|
|
|
const feedback = addVariant(variant)
|
|
if (feedback) {
|
|
showToast(feedback, 'error')
|
|
return
|
|
}
|
|
|
|
showToast(t('storeItems.addedToCart'), 'success')
|
|
}
|
|
|
|
function handleVariantPicked(variant: StoreItem) {
|
|
const feedback = addVariant(variant)
|
|
setPickVariantTarget(null)
|
|
if (feedback) {
|
|
showToast(feedback, 'error')
|
|
return
|
|
}
|
|
showToast(t('storeItems.addedToCart'), 'success')
|
|
}
|
|
|
|
return (
|
|
<main className={pageStyles.content}>
|
|
<Breadcrumbs
|
|
items={[
|
|
{ label: 'Dashboard', href: '/' },
|
|
{ label: 'Website', href: '/website' },
|
|
{ label: 'Special Items' },
|
|
]}
|
|
/>
|
|
|
|
<div className={pageStyles.pageHeader}>
|
|
<div>
|
|
<h2 className={pageStyles.pageTitle}>{t('title.specialItems')}</h2>
|
|
<p className={pageStyles.pageSubtitle}>{t('website.specialItems.subtitle')}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{error && <p className={styles.error}>{error}</p>}
|
|
|
|
{isLoading ? (
|
|
<p className={styles.empty}>{t('website.specialItems.loading')}</p>
|
|
) : specials.length === 0 ? (
|
|
<p className={styles.empty}>{t('website.specialItems.empty')}</p>
|
|
) : (
|
|
<div className={styles.carousels}>
|
|
{specials.map((special) => (
|
|
<StoreSpecialCarousel
|
|
key={special.id}
|
|
special={special}
|
|
onAddItems={setPickItemsTarget}
|
|
onEditSpecial={setEditSpecialTarget}
|
|
onDeleteSpecial={setDeleteSpecialTarget}
|
|
onOpenListing={setEditListingTarget}
|
|
onEditListing={setEditListingTarget}
|
|
onDiscountListing={setDiscountTarget}
|
|
onFestivalListing={setFestivalTarget}
|
|
onRemoveListing={(specialEntry, listing) =>
|
|
setRemoveTarget({ special: specialEntry, listing })
|
|
}
|
|
onAddToCart={handleAddToCart}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className={fabStyles.fabDock}>
|
|
{hasItems && (
|
|
<button
|
|
type="button"
|
|
className={fabStyles.cartFabStrip}
|
|
onClick={() => setCartOpen(true)}
|
|
aria-label={t('storeItems.cartOpen', { count: itemCount })}
|
|
>
|
|
<span className={fabStyles.cartFabCount}>{itemCount}</span>
|
|
<span className={fabStyles.cartFabLabel}>
|
|
{itemCount === 1 ? t('storeItems.cartOne') : t('storeItems.cartMany')}
|
|
</span>
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
className={fabStyles.addFab}
|
|
onClick={() => setCreateOpen(true)}
|
|
aria-label={t('website.specialItems.addFab')}
|
|
>
|
|
<Plus size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
<StoreSpecialModal
|
|
open={createOpen}
|
|
onClose={() => !isSaving && setCreateOpen(false)}
|
|
onSubmit={handleCreateSpecial}
|
|
title={t('website.specialItems.createTitle')}
|
|
submitLabel={t('website.create')}
|
|
isSubmitting={isSaving}
|
|
/>
|
|
|
|
<StoreSpecialModal
|
|
open={!!editSpecialTarget}
|
|
onClose={() => !isSaving && setEditSpecialTarget(null)}
|
|
onSubmit={handleEditSpecial}
|
|
initialKey={editSpecialTarget?.key ?? ''}
|
|
initialTitle={editSpecialTarget?.title ?? ''}
|
|
title={t('website.specialItems.editTitle')}
|
|
submitLabel={t('website.save')}
|
|
isSubmitting={isSaving}
|
|
/>
|
|
|
|
<PickStoreSpecialItemsModal
|
|
open={!!pickItemsTarget}
|
|
specialTitle={pickItemsTarget?.title ?? ''}
|
|
existingStoreItemIds={pickItemsTarget?.items.map((item) => item.id) ?? []}
|
|
onClose={() => !isSaving && setPickItemsTarget(null)}
|
|
onConfirm={handleAddItems}
|
|
isSubmitting={isSaving}
|
|
/>
|
|
|
|
<EditStoreItemsModal
|
|
open={!!editListingTarget}
|
|
productTitle={editListingTarget?.productTitle ?? ''}
|
|
productNameFa={editListingTarget?.productNameFa ?? ''}
|
|
items={productItems}
|
|
onClose={() => setEditListingTarget(null)}
|
|
onSaved={replaceProductItems}
|
|
/>
|
|
|
|
<StoreItemDiscountModal
|
|
open={!!discountTarget}
|
|
productTitle={discountTarget?.productTitle ?? ''}
|
|
items={productItems}
|
|
onClose={() => setDiscountTarget(null)}
|
|
onSaved={mergeUpdatedItems}
|
|
/>
|
|
|
|
<StoreItemFestivalModal
|
|
open={!!festivalTarget}
|
|
productTitle={festivalTarget?.productTitle ?? ''}
|
|
items={productItems}
|
|
onClose={() => setFestivalTarget(null)}
|
|
onSaved={mergeUpdatedItems}
|
|
/>
|
|
|
|
<ConfirmDeleteModal
|
|
open={!!deleteSpecialTarget}
|
|
title={t('website.specialItems.deleteTitle')}
|
|
message={
|
|
deleteSpecialTarget
|
|
? t('website.specialItems.deleteMessage', { title: deleteSpecialTarget.title })
|
|
: ''
|
|
}
|
|
onConfirm={() => void confirmDeleteSpecial()}
|
|
onCancel={() => !isSaving && setDeleteSpecialTarget(null)}
|
|
/>
|
|
|
|
<ConfirmDeleteModal
|
|
open={!!removeTarget}
|
|
title={t('website.specialItems.removeTitle')}
|
|
message={
|
|
removeTarget
|
|
? t('website.specialItems.removeMessage', {
|
|
name: removeTarget.listing.productTitle,
|
|
group: removeTarget.special.title,
|
|
})
|
|
: ''
|
|
}
|
|
onConfirm={() => void confirmRemoveFromSpecial()}
|
|
onCancel={() => !isSaving && setRemoveTarget(null)}
|
|
/>
|
|
|
|
<PickStoreVariantModal
|
|
open={!!pickVariantTarget}
|
|
productTitle={pickVariantTarget?.productTitle ?? ''}
|
|
productNameFa={pickVariantTarget?.productNameFa ?? ''}
|
|
variants={pickVariantTarget?.variants ?? []}
|
|
onClose={() => setPickVariantTarget(null)}
|
|
onSelect={handleVariantPicked}
|
|
/>
|
|
|
|
<ShoppingCartModal
|
|
open={cartOpen}
|
|
onClose={() => setCartOpen(false)}
|
|
onOrderCreated={() => void loadSpecials()}
|
|
/>
|
|
</main>
|
|
)
|
|
}
|