Files
dashboards/apps/customer/src/services/userProductsService.ts
T
Alireza HassaniandCursor 592540cc7e Add multi_value technical inputs and FA AI prompts.
Support duplicatable list fields in form builder and value UIs, and guide fill prompts to put leftover specs in سایر ویژگی‌ها.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-19 13:50:13 +03:30

266 lines
6.6 KiB
TypeScript

import { apiRequest } from '../lib/api'
import { getActiveBusinessId } from '../lib/businessContext'
import type { UserProductListItem } 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'
| 'multi_value'
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
}
export interface ListMyUserProductsParams {
page?: number
pageSize?: number
}
export interface MyUserProductsListResponse {
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}/my-user-products${suffix}`
}
/** Decode free-text list values stored as JSON in text_value. */
export function decodeMultiValueText(raw: string | null | undefined): string[] {
if (raw == null) return []
const trimmed = raw.trim()
if (!trimmed) return []
if (trimmed.startsWith('[')) {
try {
const parsed = JSON.parse(trimmed) as unknown
if (Array.isArray(parsed)) {
return parsed.map((item) => String(item).trim()).filter(Boolean)
}
} catch {
// fall through
}
}
return [trimmed]
}
export function hydrateTechnicalFormValues(
fields: TechnicalFormField[],
values: TechnicalFormValues,
): TechnicalFormValues {
const next: TechnicalFormValues = { ...values }
for (const field of fields) {
if (field.type !== 'multi_value') continue
const current = next[field.id]
if (Array.isArray(current)) continue
if (typeof current === 'string') {
next[field.id] = decodeMultiValueText(current)
} else {
next[field.id] = []
}
}
return next
}
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 === 'multi_value') {
const items = Array.isArray(value)
? value.map((item) => String(item).trim()).filter(Boolean)
: []
if (!items.length) continue
payload.push({ fieldId: field.id, textValue: JSON.stringify(items) })
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 listMyUserProducts(
params: ListMyUserProductsParams = {},
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))
const query = q.toString()
return apiRequest<MyUserProductsListResponse>(
`${businessPath()}${query ? `?${query}` : ''}`,
{ auth: true, signal },
)
}
export async function createMyUserProduct(input: CreateUserProductInput) {
return apiRequest<{ message: string; product: UserProductListItem }>(
businessPath(),
{
method: 'POST',
auth: true,
body: input,
},
)
}
export async function getMyUserProduct(
productId: string,
signal?: AbortSignal,
) {
return apiRequest<{ product: UserProductDetail }>(
businessPath(`/${productId}`),
{ auth: true, signal },
)
}
export async function updateMyUserProduct(
productId: string,
input: UpdateUserProductInput,
) {
return apiRequest<{ message: string; product: UserProductListItem }>(
businessPath(`/${productId}`),
{
method: 'PATCH',
auth: true,
body: input,
},
)
}
export async function deleteMyUserProduct(productId: string) {
return apiRequest<{ message: string }>(businessPath(`/${productId}`), {
method: 'DELETE',
auth: true,
})
}
export async function promoteMyUserProduct(productId: string) {
return apiRequest<{ message: string; product: UserProductListItem }>(
businessPath(`/${productId}/promote`),
{
method: 'POST',
auth: true,
},
)
}
export async function listMyUserProductCategories(signal?: AbortSignal) {
return apiRequest<{ items: UserProductCategoryOption[] }>(
businessPath('/categories'),
{ auth: true, signal },
)
}
export async function getMyUserProductCategoryTechnicalForm(
categoryId: string,
signal?: AbortSignal,
) {
return apiRequest<{ form: CategoryTechnicalForm | null }>(
businessPath(`/categories/${categoryId}/technical-form`),
{ auth: true, signal },
)
}