Add business workshop management UI with Jalali scheduling and shared dashboard polish.

Includes workshop list/edit/details, registrations modal, category tree, shared EmptyState/RichTextEditor/toasts, and fixes for the Jalali date picker and thumbnail field layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-22 12:22:27 +03:30
co-authored by Cursor
parent 89269dff04
commit d5aca8bea7
76 changed files with 4719 additions and 974 deletions
+60 -8
View File
@@ -22,6 +22,11 @@ import {
listStoreSpecials,
updateStoreSpecial,
} from '../services/storeSpecialService'
import {
getSettings,
type SpecialProductsSource,
} from '../services/settingsService'
import { hasBusinessModule, type BusinessModuleId } from '../utils/businessModules'
import type { StoreItem } from '../services/storeItemService'
import type { StoreSpecial } from '../types/storeSpecial'
import type { StoreProductListing } from '../utils/storeProductGroups'
@@ -29,11 +34,23 @@ import pageStyles from '../components/PageContent.module.css'
import fabStyles from './StoreItemsPage.module.css'
import styles from './StoreSpecialsPage.module.css'
function defaultSpecialProductsSource(
enabledModules: string[] | undefined,
): SpecialProductsSource {
return hasBusinessModule(
enabledModules as BusinessModuleId[] | undefined,
'store',
)
? 'store_item'
: 'product'
}
export function StoreSpecialsPage() {
const t = useT()
const { itemCount, hasItems, addVariant } = useDraftCart()
const { showToast } = useToast()
const [specials, setSpecials] = useState<StoreSpecial[]>([])
const [specialSource, setSpecialSource] = useState<SpecialProductsSource>('store_item')
const [isLoading, setIsLoading] = useState(true)
const [isSaving, setIsSaving] = useState(false)
const [error, setError] = useState('')
@@ -61,10 +78,36 @@ export function StoreSpecialsPage() {
useEffect(() => {
const controller = new AbortController()
void loadSpecials(controller.signal)
void loadPage(controller.signal)
return () => controller.abort()
}, [])
async function loadPage(signal?: AbortSignal) {
setIsLoading(true)
setError('')
try {
const [specialsData, settingsData] = await Promise.all([
listStoreSpecials(1, 50, signal),
getSettings(signal),
])
setSpecials(specialsData.items)
setSpecialSource(
settingsData.settings.website?.specialProductsSource ??
defaultSpecialProductsSource(settingsData.settings.modules?.enabled),
)
} 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)
}
}
async function loadSpecials(signal?: AbortSignal) {
setIsLoading(true)
setError('')
@@ -212,19 +255,23 @@ export function StoreSpecialsPage() {
}
}
async function handleAddItems(storeItemIds: string[]) {
async function handleAddItems(ids: string[]) {
if (!pickItemsTarget) return
const existingIds = pickItemsTarget.items.map((item) => item.id)
const nextIds = [...existingIds, ...storeItemIds]
const fromProducts = specialSource === 'product'
const existingIds = pickItemsTarget.items.map((item) =>
fromProducts ? item.productId : item.id,
)
const nextIds = [...existingIds, ...ids]
setIsSaving(true)
setError('')
try {
const result = await updateStoreSpecial(pickItemsTarget.id, {
storeItemIds: nextIds,
})
const result = await updateStoreSpecial(
pickItemsTarget.id,
fromProducts ? { productIds: nextIds } : { storeItemIds: nextIds },
)
replaceSpecial(result.special)
setPickItemsTarget(null)
showToast(t('website.specialItems.toastAdded'), 'success')
@@ -402,7 +449,12 @@ export function StoreSpecialsPage() {
<PickStoreSpecialItemsModal
open={!!pickItemsTarget}
specialTitle={pickItemsTarget?.title ?? ''}
existingStoreItemIds={pickItemsTarget?.items.map((item) => item.id) ?? []}
source={specialSource}
existingIds={
pickItemsTarget?.items.map((item) =>
specialSource === 'product' ? item.productId : item.id,
) ?? []
}
onClose={() => !isSaving && setPickItemsTarget(null)}
onConfirm={handleAddItems}
isSubmitting={isSaving}