mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-12 06:40:57 +04:30
Initial commit: Meshkee dashboards monorepo.
Includes business, customer, and super-admin apps with shared packages and production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,461 @@
|
||||
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 { 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 { 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('Unable to load special categories.')
|
||||
}
|
||||
} 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(title: string) {
|
||||
setIsSaving(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const result = await createStoreSpecial({
|
||||
title,
|
||||
sortOrder: specials.length,
|
||||
})
|
||||
setSpecials((prev) => [...prev, result.special])
|
||||
setCreateOpen(false)
|
||||
showToast('Special category created.', 'success')
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError('Unable to create special category.')
|
||||
}
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEditSpecial(title: string) {
|
||||
if (!editSpecialTarget) return
|
||||
|
||||
setIsSaving(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const result = await updateStoreSpecial(editSpecialTarget.id, { title })
|
||||
replaceSpecial(result.special)
|
||||
setEditSpecialTarget(null)
|
||||
showToast('Special category updated.', 'success')
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError('Unable to update special category.')
|
||||
}
|
||||
} 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('Special category deleted.', 'success')
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError('Unable to delete special category.')
|
||||
}
|
||||
} 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('Store items added to special category.', 'success')
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError('Unable to add store items.')
|
||||
}
|
||||
} 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('Removed from special category.', 'success')
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError('Unable to remove store item.')
|
||||
}
|
||||
} 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('Added to shopping cart.', 'success')
|
||||
}
|
||||
|
||||
function handleVariantPicked(variant: StoreItem) {
|
||||
const feedback = addVariant(variant)
|
||||
setPickVariantTarget(null)
|
||||
if (feedback) {
|
||||
showToast(feedback, 'error')
|
||||
return
|
||||
}
|
||||
showToast('Added to shopping cart.', '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}>Special Items</h2>
|
||||
<p className={pageStyles.pageSubtitle}>
|
||||
Curate featured store items into categories for your website carousels.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <p className={styles.error}>{error}</p>}
|
||||
|
||||
{isLoading ? (
|
||||
<p className={styles.empty}>Loading special categories...</p>
|
||||
) : specials.length === 0 ? (
|
||||
<p className={styles.empty}>
|
||||
No special categories yet. Use the + button to create one, then add store items to each
|
||||
carousel.
|
||||
</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={`Open shopping cart, ${itemCount} items`}
|
||||
>
|
||||
<span className={fabStyles.cartFabCount}>{itemCount}</span>
|
||||
<span className={fabStyles.cartFabLabel}>
|
||||
{itemCount === 1 ? 'item in cart' : 'items in cart'}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={fabStyles.addFab}
|
||||
onClick={() => setCreateOpen(true)}
|
||||
aria-label="Add special category"
|
||||
>
|
||||
<Plus size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<StoreSpecialModal
|
||||
open={createOpen}
|
||||
onClose={() => !isSaving && setCreateOpen(false)}
|
||||
onSubmit={handleCreateSpecial}
|
||||
isSubmitting={isSaving}
|
||||
/>
|
||||
|
||||
<StoreSpecialModal
|
||||
open={!!editSpecialTarget}
|
||||
onClose={() => !isSaving && setEditSpecialTarget(null)}
|
||||
onSubmit={handleEditSpecial}
|
||||
initialTitle={editSpecialTarget?.title ?? ''}
|
||||
title="Edit Special Category"
|
||||
submitLabel="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="Delete Special Category"
|
||||
message={
|
||||
deleteSpecialTarget
|
||||
? `Delete "${deleteSpecialTarget.title}"? Store items will remain in your catalog.`
|
||||
: ''
|
||||
}
|
||||
onConfirm={() => void confirmDeleteSpecial()}
|
||||
onCancel={() => !isSaving && setDeleteSpecialTarget(null)}
|
||||
/>
|
||||
|
||||
<ConfirmDeleteModal
|
||||
open={!!removeTarget}
|
||||
title="Remove from Special"
|
||||
message={
|
||||
removeTarget
|
||||
? `Remove "${removeTarget.listing.productTitle}" from "${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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user