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>
219 lines
5.8 KiB
TypeScript
219 lines
5.8 KiB
TypeScript
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 },
|
|
)
|
|
}
|