Add customer and business user-product flows across dashboards.
Ship my-products / customer-products UI with gallery uploads, status controls, module gating, and related shared UI polish. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
c5bdaad16b
commit
1271d96539
@@ -18,3 +18,17 @@ export async function listCitiesByProvinceSlug(parentSlug: string, signal?: Abor
|
||||
)
|
||||
return data.items
|
||||
}
|
||||
|
||||
|
||||
export async function listCountries(signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ items: CityOption[] }>('/cities?level=country', { signal })
|
||||
return data.items
|
||||
}
|
||||
|
||||
export async function listCitiesByCountrySlug(parentSlug: string, signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ items: CityOption[] }>(
|
||||
`/cities?level=city&parentSlug=${encodeURIComponent(parentSlug)}`,
|
||||
{ signal },
|
||||
)
|
||||
return data.items
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type { UserProductListItem, UserProductStatus } from '../types/userProduct'
|
||||
|
||||
export type UserProductCondition = 'new' | 'stock' | 'needs_repair' | 'scrap'
|
||||
export type UserProductPriceCurrency = 'IRT' | 'USD' | 'EUR' | 'AED'
|
||||
export type TechnicalFieldType = 'text' | 'textarea' | 'select' | 'multi_select'
|
||||
|
||||
export interface UserProductCategoryOption {
|
||||
id: string
|
||||
name: string
|
||||
nameFa: string | null
|
||||
parentId: string | null
|
||||
}
|
||||
|
||||
export interface TechnicalFormFieldOption {
|
||||
id: string
|
||||
label: string
|
||||
value: string
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
export interface TechnicalFormField {
|
||||
id: string
|
||||
label: string
|
||||
key: string
|
||||
type: TechnicalFieldType
|
||||
isRequired: boolean
|
||||
sortOrder: number
|
||||
options: TechnicalFormFieldOption[]
|
||||
}
|
||||
|
||||
export interface CategoryTechnicalForm {
|
||||
id: string
|
||||
categoryId: string
|
||||
fields: TechnicalFormField[]
|
||||
}
|
||||
|
||||
export type TechnicalFormValues = Record<string, string | string[]>
|
||||
|
||||
export interface UserProductTechnicalValueInput {
|
||||
fieldId: string
|
||||
textValue?: string
|
||||
optionId?: string
|
||||
optionIds?: string[]
|
||||
}
|
||||
|
||||
export interface CreateUserProductInput {
|
||||
titleFa: string
|
||||
titleEn?: string
|
||||
description?: string
|
||||
categoryId: string
|
||||
price?: number
|
||||
priceCurrency?: UserProductPriceCurrency
|
||||
priceByExpert?: boolean
|
||||
countryId: string
|
||||
cityId: string
|
||||
deliveryNote?: string
|
||||
condition: UserProductCondition
|
||||
technicalNotes?: string
|
||||
technicalValues?: UserProductTechnicalValueInput[]
|
||||
featuredMediaId?: string
|
||||
galleryMediaIds?: string[]
|
||||
}
|
||||
|
||||
export type UpdateUserProductInput = CreateUserProductInput
|
||||
|
||||
export interface UserProductDetail extends UserProductListItem {
|
||||
countryId: string
|
||||
cityId: string
|
||||
countrySlug: string
|
||||
featuredMediaId: string | null
|
||||
galleryMediaIds: string[]
|
||||
images: Array<{ mediaId: string; url: string }>
|
||||
technicalValues: UserProductTechnicalValueInput[]
|
||||
deliveryNote?: string | null
|
||||
technicalNotes?: string | null
|
||||
description?: string | null
|
||||
titleEn?: string | null
|
||||
priceCurrency?: string | null
|
||||
priceByExpert?: boolean
|
||||
promoted?: boolean
|
||||
condition?: string | null
|
||||
categoryId?: string | null
|
||||
}
|
||||
|
||||
export interface ListUserProductsParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: UserProductStatus
|
||||
}
|
||||
|
||||
export interface UserProductsListResponse {
|
||||
items: UserProductListItem[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/user-products${suffix}`
|
||||
}
|
||||
|
||||
export function buildTechnicalValuesPayload(
|
||||
fields: TechnicalFormField[],
|
||||
values: TechnicalFormValues,
|
||||
): UserProductTechnicalValueInput[] {
|
||||
const payload: UserProductTechnicalValueInput[] = []
|
||||
|
||||
for (const field of fields) {
|
||||
const value = values[field.id]
|
||||
|
||||
if (field.type === 'text' || field.type === 'textarea') {
|
||||
const text = typeof value === 'string' ? value.trim() : ''
|
||||
if (!text) continue
|
||||
payload.push({ fieldId: field.id, textValue: text })
|
||||
continue
|
||||
}
|
||||
|
||||
if (field.type === 'select') {
|
||||
const optionId = typeof value === 'string' ? value.trim() : ''
|
||||
if (!optionId) continue
|
||||
payload.push({ fieldId: field.id, optionId })
|
||||
continue
|
||||
}
|
||||
|
||||
const optionIds = Array.isArray(value) ? value.filter(Boolean) : []
|
||||
if (!optionIds.length) continue
|
||||
payload.push({ fieldId: field.id, optionIds })
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
export async function listUserProducts(
|
||||
params: ListUserProductsParams = {},
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const q = new URLSearchParams()
|
||||
if (params.page !== undefined) q.set('page', String(params.page))
|
||||
if (params.pageSize !== undefined) q.set('pageSize', String(params.pageSize))
|
||||
if (params.status) q.set('status', params.status)
|
||||
|
||||
const query = q.toString()
|
||||
return apiRequest<UserProductsListResponse>(
|
||||
`${businessPath()}${query ? `?${query}` : ''}`,
|
||||
{ auth: true, signal },
|
||||
)
|
||||
}
|
||||
|
||||
export async function getUserProduct(productId: string, signal?: AbortSignal) {
|
||||
return apiRequest<{ product: UserProductDetail }>(
|
||||
businessPath(`/${productId}`),
|
||||
{ auth: true, signal },
|
||||
)
|
||||
}
|
||||
|
||||
export async function createUserProduct(input: CreateUserProductInput) {
|
||||
return apiRequest<{ message: string; product: UserProductListItem }>(
|
||||
businessPath(),
|
||||
{ method: 'POST', auth: true, body: input },
|
||||
)
|
||||
}
|
||||
|
||||
export async function updateUserProduct(
|
||||
productId: string,
|
||||
input: UpdateUserProductInput,
|
||||
) {
|
||||
return apiRequest<{ message: string; product: UserProductListItem }>(
|
||||
businessPath(`/${productId}`),
|
||||
{ method: 'PATCH', auth: true, body: input },
|
||||
)
|
||||
}
|
||||
|
||||
export async function updateUserProductStatus(
|
||||
productId: string,
|
||||
status: UserProductStatus,
|
||||
) {
|
||||
return apiRequest<{ message: string; product: UserProductListItem }>(
|
||||
businessPath(`/${productId}/status`),
|
||||
{ method: 'PATCH', auth: true, body: { status } },
|
||||
)
|
||||
}
|
||||
|
||||
export async function promoteUserProduct(productId: string) {
|
||||
return apiRequest<{ message: string; product: UserProductListItem }>(
|
||||
businessPath(`/${productId}/promote`),
|
||||
{ method: 'POST', auth: true },
|
||||
)
|
||||
}
|
||||
|
||||
export async function deleteUserProduct(productId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${productId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
export async function listUserProductCategories(signal?: AbortSignal) {
|
||||
return apiRequest<{ items: UserProductCategoryOption[] }>(
|
||||
businessPath('/categories'),
|
||||
{ auth: true, signal },
|
||||
)
|
||||
}
|
||||
|
||||
export async function getUserProductCategoryTechnicalForm(
|
||||
categoryId: string,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
return apiRequest<{ form: CategoryTechnicalForm | null }>(
|
||||
businessPath(`/categories/${categoryId}/technical-form`),
|
||||
{ auth: true, signal },
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user