Wire dashboards to the API and add customer portal plus discounts.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
af11d96fd0
commit
b103e330ef
+20
-3
@@ -118,16 +118,33 @@ export function removeCategory(
|
||||
}))
|
||||
}
|
||||
|
||||
export type FlatCategoryOption = {
|
||||
id: string
|
||||
nameFa: string
|
||||
nameEn: string
|
||||
labelFa: string
|
||||
labelEn: string
|
||||
depth: number
|
||||
}
|
||||
|
||||
export function flattenCategories(
|
||||
categories: Category[],
|
||||
prefix = '',
|
||||
): { id: string; labelFa: string; labelEn: string }[] {
|
||||
depth = 0,
|
||||
): FlatCategoryOption[] {
|
||||
return categories.flatMap((category) => {
|
||||
const labelFa = prefix ? `${prefix} / ${category.nameFa}` : category.nameFa
|
||||
const labelEn = prefix ? `${prefix} / ${category.nameEn}` : category.nameEn
|
||||
return [
|
||||
{ id: category.id, labelFa, labelEn },
|
||||
...flattenCategories(category.children, labelFa),
|
||||
{
|
||||
id: category.id,
|
||||
nameFa: category.nameFa,
|
||||
nameEn: category.nameEn,
|
||||
labelFa,
|
||||
labelEn,
|
||||
depth,
|
||||
},
|
||||
...flattenCategories(category.children, labelFa, depth + 1),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
+34
-146
@@ -11,12 +11,14 @@ export type OrderStatus =
|
||||
export type DeliveryType = 'shipping' | 'pickup'
|
||||
|
||||
export type OrderItemOption = {
|
||||
id?: string
|
||||
name: string
|
||||
price: number
|
||||
}
|
||||
|
||||
export type OrderItem = {
|
||||
id: string
|
||||
productId?: string | null
|
||||
nameFa: string
|
||||
quantity: number
|
||||
unitPrice: number
|
||||
@@ -40,11 +42,15 @@ export type OrderDelivery =
|
||||
| {
|
||||
type: 'pickup'
|
||||
branch: string
|
||||
branchId?: string | null
|
||||
}
|
||||
|
||||
export type Order = {
|
||||
id: string
|
||||
code: string
|
||||
number: number
|
||||
createdAt: string
|
||||
customerId: string
|
||||
customerName: string
|
||||
customerPhone: string
|
||||
itemCount: number
|
||||
@@ -53,6 +59,8 @@ export type Order = {
|
||||
delivery: OrderDelivery
|
||||
items: OrderItem[]
|
||||
note?: string
|
||||
discountCode?: string
|
||||
discountAmount?: number
|
||||
}
|
||||
|
||||
export const orderStatuses: { id: OrderStatus; label: string }[] = [
|
||||
@@ -83,84 +91,36 @@ export const orderSellUnitLabel: Record<SellUnit, string> = {
|
||||
kilo: 'کیلو',
|
||||
}
|
||||
|
||||
const customers = [
|
||||
{ name: 'سارا محمدی', phone: '09121234567' },
|
||||
{ name: 'علی رضایی', phone: '09129876543' },
|
||||
{ name: 'مریم حسینی', phone: '09351234567' },
|
||||
{ name: 'حسین کریمی', phone: '09123456789' },
|
||||
{ name: 'نازنین احمدی', phone: '09127654321' },
|
||||
{ name: 'رضا نوری', phone: '09361234567' },
|
||||
{ name: 'فاطمه جعفری', phone: '09121112233' },
|
||||
{ name: 'امیر صادقی', phone: '09125556677' },
|
||||
]
|
||||
/** Aggregate order quantities by sell unit for list display. */
|
||||
export function formatOrderQuantityByUnit(
|
||||
items: Array<{ quantity: number; sellUnit: SellUnit }>,
|
||||
): string[] {
|
||||
let unitTotal = 0
|
||||
let kiloTotal = 0
|
||||
|
||||
const productNames = [
|
||||
'شیرینی پسته',
|
||||
'کیک شکلاتی',
|
||||
'باقلوا گردو',
|
||||
'نان بربری',
|
||||
'ماکارون',
|
||||
'شیرینی نخودچی',
|
||||
'کیک هویج',
|
||||
'رولت خامه',
|
||||
]
|
||||
for (const item of items) {
|
||||
if (item.sellUnit === 'kilo') kiloTotal += item.quantity
|
||||
else unitTotal += item.quantity
|
||||
}
|
||||
|
||||
const optionChoices: OrderItemOption[] = [
|
||||
{ name: 'شکلات اضافه', price: 50000 },
|
||||
{ name: 'بسته کادویی', price: 25000 },
|
||||
{ name: 'نوع شکلات تلخ', price: 40000 },
|
||||
{ name: 'وزن ۱ کیلو', price: 80000 },
|
||||
{ name: 'روکش شکلات', price: 35000 },
|
||||
]
|
||||
|
||||
const branches = ['شعبه مرکزی', 'شعبه صفاییه', 'شعبه نیروی هوایی']
|
||||
|
||||
const shippingAddresses: ShippingAddress[] = [
|
||||
{
|
||||
id: 'addr-1',
|
||||
name: 'منزل',
|
||||
district: 'خیابان ارم',
|
||||
address: 'قم، خیابان ارم، کوچه ۵، پلاک ۱۲',
|
||||
landline: '02531234567',
|
||||
},
|
||||
{
|
||||
id: 'addr-2',
|
||||
name: 'محل کار',
|
||||
district: 'بلوار امین',
|
||||
address: 'قم، بلوار امین، مجتمع تجاری نور، واحد ۴',
|
||||
landline: '02537654321',
|
||||
},
|
||||
{
|
||||
id: 'addr-3',
|
||||
name: 'منزل',
|
||||
district: 'صفاییه',
|
||||
address: 'قم، خیابان شهید بهشتی، پلاک ۸۷',
|
||||
landline: '02531112233',
|
||||
},
|
||||
{
|
||||
id: 'addr-4',
|
||||
name: 'منزل والدین',
|
||||
district: 'حرم',
|
||||
address: 'قم، میدان جهاد، خیابان امام، پلاک ۲۳',
|
||||
landline: '02534445566',
|
||||
},
|
||||
]
|
||||
|
||||
export function formatShippingAddress(address: ShippingAddress) {
|
||||
const landline = address.landline
|
||||
? ` — ${address.landline}`
|
||||
: ''
|
||||
return `${address.name} · ${address.district} — ${address.address}${landline}`
|
||||
const lines: string[] = []
|
||||
if (unitTotal > 0) {
|
||||
lines.push(
|
||||
`${unitTotal.toLocaleString('fa-IR')} ${orderSellUnitLabel.unit}`,
|
||||
)
|
||||
}
|
||||
if (kiloTotal > 0) {
|
||||
lines.push(
|
||||
`${kiloTotal.toLocaleString('fa-IR')} ${orderSellUnitLabel.kilo}`,
|
||||
)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
const statuses: OrderStatus[] = [
|
||||
'pending',
|
||||
'confirmed',
|
||||
'preparing',
|
||||
'ready',
|
||||
'delivered',
|
||||
'cancelled',
|
||||
]
|
||||
export function formatShippingAddress(address: ShippingAddress) {
|
||||
const landline = address.landline ? ` — ${address.landline}` : ''
|
||||
return `${address.name} · ${address.district} — ${address.address}${landline}`
|
||||
}
|
||||
|
||||
export function getItemUnitTotal(item: OrderItem) {
|
||||
const optionsTotal = (item.options ?? []).reduce(
|
||||
@@ -174,78 +134,6 @@ export function getItemLineTotal(item: OrderItem) {
|
||||
return item.quantity * getItemUnitTotal(item)
|
||||
}
|
||||
|
||||
function buildItems(seed: number): OrderItem[] {
|
||||
const count = (seed % 4) + 1
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const name = productNames[(seed + index) % productNames.length]
|
||||
const quantity = ((seed + index) % 3) + 1
|
||||
const unitPrice = [95000, 120000, 185000, 265000, 310000, 420000][
|
||||
(seed + index) % 6
|
||||
]
|
||||
const sellUnit: SellUnit =
|
||||
name.includes('کیک') || name.includes('باقلوا') ? 'kilo' : 'unit'
|
||||
const withOptions = (seed + index) % 3 !== 0
|
||||
const firstOption = optionChoices[(seed + index) % optionChoices.length]
|
||||
const secondOption =
|
||||
optionChoices[(seed + index + 2) % optionChoices.length]
|
||||
const chosenOptions =
|
||||
(seed + index) % 2 === 0 && firstOption.name !== secondOption.name
|
||||
? [firstOption, secondOption]
|
||||
: [firstOption]
|
||||
return {
|
||||
id: `item-${seed}-${index}`,
|
||||
nameFa: name,
|
||||
quantity,
|
||||
unitPrice,
|
||||
sellUnit,
|
||||
options: withOptions ? chosenOptions : undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function buildDelivery(index: number): OrderDelivery {
|
||||
if (index % 2 === 0) {
|
||||
return {
|
||||
type: 'shipping',
|
||||
shippingAddress: shippingAddresses[index % shippingAddresses.length],
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'pickup',
|
||||
branch: branches[index % branches.length],
|
||||
}
|
||||
}
|
||||
|
||||
function buildOrders(): Order[] {
|
||||
return Array.from({ length: 28 }, (_, index) => {
|
||||
const customer = customers[index % customers.length]
|
||||
const items = buildItems(index + 1)
|
||||
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)
|
||||
const totalPrice = items.reduce(
|
||||
(sum, item) => sum + getItemLineTotal(item),
|
||||
0,
|
||||
)
|
||||
const day = String((index % 28) + 1).padStart(2, '0')
|
||||
const hour = String(10 + (index % 10)).padStart(2, '0')
|
||||
const minute = String((index * 7) % 60).padStart(2, '0')
|
||||
|
||||
return {
|
||||
id: `BL-${1400 + index}`,
|
||||
createdAt: `2026-07-${day}T${hour}:${minute}:00`,
|
||||
customerName: customer.name,
|
||||
customerPhone: customer.phone,
|
||||
itemCount,
|
||||
totalPrice,
|
||||
status: statuses[index % statuses.length],
|
||||
delivery: buildDelivery(index),
|
||||
items,
|
||||
note: index % 5 === 0 ? 'تحویل در ساعت ۱۷' : undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const orders: Order[] = buildOrders()
|
||||
|
||||
export function formatOrderDateTime(iso: string): { date: string; time: string } {
|
||||
const date = new Date(iso)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
|
||||
+39
-99
@@ -1,17 +1,29 @@
|
||||
import img1010 from '../images/products/IMG_1010.jpeg'
|
||||
import img1031 from '../images/products/IMG_1031.jpeg'
|
||||
import img1035 from '../images/products/IMG_1035.jpeg'
|
||||
import img1039 from '../images/products/IMG_1039.jpeg'
|
||||
import img1040 from '../images/products/IMG_1040.jpeg'
|
||||
import img1043 from '../images/products/IMG_1043.jpeg'
|
||||
|
||||
export type SellUnit = 'unit' | 'kilo'
|
||||
|
||||
export type ProductCategoryRef = {
|
||||
id: string
|
||||
nameFa: string
|
||||
nameEn: string
|
||||
parentId?: string | null
|
||||
}
|
||||
|
||||
export type ProductOptionValue = {
|
||||
id: string
|
||||
flavorId: string
|
||||
amount: string
|
||||
price: number
|
||||
flavor?: {
|
||||
id: string
|
||||
nameFa: string
|
||||
nameEn: string
|
||||
}
|
||||
}
|
||||
|
||||
export type ProductGalleryImage = {
|
||||
id?: string
|
||||
url: string
|
||||
storageKey: string
|
||||
sortOrder?: number
|
||||
}
|
||||
|
||||
export type Product = {
|
||||
@@ -20,18 +32,25 @@ export type Product = {
|
||||
nameEn: string
|
||||
price: number
|
||||
sellUnit: SellUnit
|
||||
category: string
|
||||
image: string
|
||||
categoryId: string
|
||||
category: ProductCategoryRef
|
||||
intro?: string
|
||||
description?: string
|
||||
tags?: string[]
|
||||
mainImageUrl: string | null
|
||||
mainImageKey?: string | null
|
||||
gallery?: ProductGalleryImage[]
|
||||
options: ProductOptionValue[]
|
||||
}
|
||||
|
||||
export const productCategories = [
|
||||
'شیرینی',
|
||||
'کیک',
|
||||
'باقلوا',
|
||||
'نان',
|
||||
'ماکارون',
|
||||
] as const
|
||||
export const productImagePlaceholder =
|
||||
'data:image/svg+xml,' +
|
||||
encodeURIComponent(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 400 400">
|
||||
<rect width="400" height="400" fill="#f3ebe3"/>
|
||||
<text x="200" y="210" text-anchor="middle" fill="#b08a6a" font-family="sans-serif" font-size="28">محصول</text>
|
||||
</svg>`,
|
||||
)
|
||||
|
||||
function optionId() {
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
|
||||
@@ -50,89 +69,6 @@ export function createProductOptionValue(
|
||||
}
|
||||
}
|
||||
|
||||
export const products: Product[] = [
|
||||
{
|
||||
id: '1',
|
||||
nameFa: 'شیرینی پسته',
|
||||
nameEn: 'Pistachio Cookie',
|
||||
price: 185000,
|
||||
sellUnit: 'unit',
|
||||
category: 'شیرینی',
|
||||
image: img1010,
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
nameFa: 'کیک شکلاتی',
|
||||
nameEn: 'Chocolate Cake',
|
||||
price: 420000,
|
||||
sellUnit: 'kilo',
|
||||
category: 'کیک',
|
||||
image: img1031,
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
nameFa: 'باقلوا گردو',
|
||||
nameEn: 'Walnut Baklava',
|
||||
price: 265000,
|
||||
sellUnit: 'kilo',
|
||||
category: 'باقلوا',
|
||||
image: img1035,
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
nameFa: 'نان خامهای',
|
||||
nameEn: 'Cream Puff',
|
||||
price: 95000,
|
||||
sellUnit: 'unit',
|
||||
category: 'نان',
|
||||
image: img1039,
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
nameFa: 'تارت میوه',
|
||||
nameEn: 'Fruit Tart',
|
||||
price: 310000,
|
||||
sellUnit: 'unit',
|
||||
category: 'کیک',
|
||||
image: img1040,
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
nameFa: 'ماکارون بلوط',
|
||||
nameEn: 'Balout Macaron',
|
||||
price: 150000,
|
||||
sellUnit: 'unit',
|
||||
category: 'ماکارون',
|
||||
image: img1043,
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
nameFa: 'کروسان کرهای',
|
||||
nameEn: 'Butter Croissant',
|
||||
price: 120000,
|
||||
sellUnit: 'unit',
|
||||
category: 'نان',
|
||||
image: img1010,
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
nameFa: 'چیزکیک وانیل',
|
||||
nameEn: 'Vanilla Cheesecake',
|
||||
price: 380000,
|
||||
sellUnit: 'kilo',
|
||||
category: 'کیک',
|
||||
image: img1040,
|
||||
options: [],
|
||||
},
|
||||
]
|
||||
|
||||
const sellUnitLabel: Record<SellUnit, string> = {
|
||||
unit: 'واحد',
|
||||
kilo: 'کیلو',
|
||||
@@ -147,3 +83,7 @@ export function formatPriceParts(product: Product): {
|
||||
label: `تومان - ${sellUnitLabel[product.sellUnit]}`,
|
||||
}
|
||||
}
|
||||
|
||||
export function productImageSrc(product: Product) {
|
||||
return product.mainImageUrl || productImagePlaceholder
|
||||
}
|
||||
|
||||
@@ -34,6 +34,19 @@ export const userTitles: UserTitle[] = [
|
||||
'سرکار خانم مهندس',
|
||||
]
|
||||
|
||||
/** Strip honorific title prefix from a full display name. */
|
||||
export function stripUserTitle(fullName: string) {
|
||||
const sorted = [...userTitles].sort((a, b) => b.length - a.length)
|
||||
const trimmed = fullName.trim()
|
||||
for (const title of sorted) {
|
||||
if (trimmed === title) return ''
|
||||
if (trimmed.startsWith(`${title} `)) {
|
||||
return trimmed.slice(title.length).trim()
|
||||
}
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
export const userRoles: { id: UserRole; label: string }[] = [
|
||||
{ id: 'customer', label: 'مشتری' },
|
||||
{ id: 'admin', label: 'ادمین' },
|
||||
|
||||
Reference in New Issue
Block a user