Files
dashboards/apps/business/src/pages/StoreSpecialsPage.tsx
T
Alireza HassaniandCursor 672091d1f5 Polish business FA layout and wire special-group keys plus SSL sync.
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>
2026-08-03 00:06:44 +03:30

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>
)
}