Add special orders admin UI and product specials tools.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-10-12 23:09:31 +03:30
co-authored by Cursor
parent b103e330ef
commit d63e3a83d5
33 changed files with 2731 additions and 73 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
import { apiUpload } from './api'
export type MediaKind = 'main' | 'gallery' | 'temp'
export type MediaKind = 'main' | 'gallery' | 'temp' | 'specialOrder'
export type MediaUploadResult = {
url: string
+23 -1
View File
@@ -1,11 +1,13 @@
import {
CakeSlice,
ClipboardPen,
FolderTree,
MapPin,
Package,
Percent,
PlusCircle,
Settings,
Sparkles,
UserRound,
Users,
type LucideIcon,
@@ -60,6 +62,14 @@ export const productTiles: ProductTile[] = [
href: '/products/categories',
icon: FolderTree,
},
{
id: 'products-specials',
title: 'محصولات ویژه',
titleEn: 'Special Products',
description: 'مشاهده و مدیریت محصولات ویژه فروشگاه',
href: '/products/specials',
icon: Sparkles,
},
]
export const navSections: NavSection[] = [
@@ -96,6 +106,15 @@ export const navSections: NavSection[] = [
icon: Package,
items: [],
},
{
id: 'special-orders',
title: 'سفارشات ویژه',
titleEn: 'Special Orders',
description: 'درخواست‌های سفارشی کیک و شیرینی بدون پرداخت',
href: '/special-orders',
icon: ClipboardPen,
items: [],
},
{
id: 'discounts',
title: 'کدهای تخفیف',
@@ -118,7 +137,10 @@ export const navSections: NavSection[] = [
/** Header links — settings & discounts stay on home tiles only. */
export const adminHeaderSections: NavSection[] = navSections.filter(
(section) => section.id !== 'settings' && section.id !== 'discounts',
(section) =>
section.id !== 'settings' &&
section.id !== 'discounts' &&
section.id !== 'special-orders',
)
export const customerNavSections: NavSection[] = [
+4
View File
@@ -18,6 +18,7 @@ export type ListProductsParams = {
categoryId?: string
minPrice?: number
maxPrice?: number
tag?: string
page?: number
pageSize?: number
}
@@ -41,6 +42,8 @@ export type CreateProductPayload = {
sellUnit: SellUnit
intro?: string
description?: string
/** null = ready immediately (no prep time) */
preparationHours?: number | null
tags?: string[]
mainImageUrl?: string
mainImageKey?: string
@@ -54,6 +57,7 @@ function buildQuery(params: ListProductsParams) {
const search = new URLSearchParams()
if (params.q?.trim()) search.set('q', params.q.trim())
if (params.categoryId) search.set('categoryId', params.categoryId)
if (params.tag?.trim()) search.set('tag', params.tag.trim())
if (params.minPrice !== undefined) search.set('minPrice', String(params.minPrice))
if (params.maxPrice !== undefined) search.set('maxPrice', String(params.maxPrice))
if (params.page) search.set('page', String(params.page))
+111
View File
@@ -0,0 +1,111 @@
import { apiRequest } from './api'
export type SpecialOrderCategory = 'specialCake' | 'specialPastry'
export type SpecialOrderImage = {
id?: string
url: string
storageKey: string
sortOrder?: number
}
export type SpecialOrderUser = {
id: string
title: string
firstName: string
lastName: string
cellNumber: string
role: string
name: string
}
export type SpecialOrder = {
id: string
userId: string
category: SpecialOrderCategory
name: string
description: string
createdAt: string
updatedAt: string
images: SpecialOrderImage[]
user: SpecialOrderUser
}
export type SpecialOrderListResponse = {
items: SpecialOrder[]
total: number
page: number
pageSize: number
}
export type ListSpecialOrdersParams = {
q?: string
userId?: string
category?: SpecialOrderCategory
page?: number
pageSize?: number
}
export type SpecialOrderPayload = {
userId: string
category: SpecialOrderCategory
name: string
description?: string
images?: Array<{ url: string; storageKey: string }>
}
export type UpdateSpecialOrderPayload = Partial<SpecialOrderPayload>
export const specialOrderCategoryLabel: Record<SpecialOrderCategory, string> = {
specialCake: 'کیک ویژه',
specialPastry: 'شیرینی ویژه',
}
export const specialOrderCategories: SpecialOrderCategory[] = [
'specialCake',
'specialPastry',
]
function buildQuery(params: ListSpecialOrdersParams) {
const search = new URLSearchParams()
if (params.q?.trim()) search.set('q', params.q.trim())
if (params.userId) search.set('userId', params.userId)
if (params.category) search.set('category', params.category)
if (params.page) search.set('page', String(params.page))
if (params.pageSize) search.set('pageSize', String(params.pageSize))
const qs = search.toString()
return qs ? `?${qs}` : ''
}
export function listSpecialOrders(params: ListSpecialOrdersParams = {}) {
return apiRequest<SpecialOrderListResponse>(
`/special-orders${buildQuery(params)}`,
)
}
export function getSpecialOrder(id: string) {
return apiRequest<SpecialOrder>(`/special-orders/${id}`)
}
export function createSpecialOrder(payload: SpecialOrderPayload) {
return apiRequest<SpecialOrder>('/special-orders', {
method: 'POST',
body: payload,
})
}
export function updateSpecialOrder(
id: string,
payload: UpdateSpecialOrderPayload,
) {
return apiRequest<SpecialOrder>(`/special-orders/${id}`, {
method: 'PATCH',
body: payload,
})
}
export function deleteSpecialOrder(id: string) {
return apiRequest<{ ok: true }>(`/special-orders/${id}`, {
method: 'DELETE',
})
}
+1 -1
View File
@@ -21,7 +21,7 @@ export type ListUsersParams = {
}
export type CreateUserPayload = {
title: UserTitle
title?: UserTitle
firstName: string
lastName: string
category?: UserCategory | null