Files
dashboards/apps/business/src/services/productService.ts
T
Alireza HassaniandCursor 672091d1f5 Polish business FA layout and wire special-group keys plus SSL sync.
RTL carousels/FABs, special key+title fields, slider gallery fixes, and dashboard SSL sync agent for super-admin.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 00:06:44 +03:30

210 lines
5.2 KiB
TypeScript

import { apiRequest } from '../lib/api'
import { getActiveBusinessId } from '../lib/businessContext'
import type { Product } from '../types/product'
export const PRODUCTS_PER_PAGE = 24
export interface ProductApi {
id: string
businessId: string
title: string
nameFa: string
summary: string
descriptionHtml: string
slug: string
status: 'draft' | 'published' | 'archived'
categoryId: string | null
categoryName: string
categoryNameFa?: string
brandId: string | null
brand: {
id: string
nameEn: string
nameFa: string | null
slug: string
imageUrl: string | null
} | null
tags: string[]
thumbnailUrl: string | null
thumbnailMediaId: string | null
image: string
thumbnail: string
images: { mediaId: string; url: string }[]
galleryMediaIds: string[]
commentCount: number
variantCount: number
createdAt: string
updatedAt: string
}
export interface ProductsListResponse {
items: ProductApi[]
total: number
page: number
pageSize: number
}
export interface CreateProductPayload {
title: string
nameFa?: string
summary?: string
descriptionHtml?: string
categoryId?: string
brandId?: string
status?: 'draft' | 'published' | 'archived'
featuredMediaId?: string
galleryMediaIds?: string[]
tags?: string[]
}
export interface UpdateProductPayload {
title?: string
nameFa?: string | null
summary?: string | null
descriptionHtml?: string | null
categoryId?: string | null
brandId?: string | null
status?: 'draft' | 'published' | 'archived'
featuredMediaId?: string | null
galleryMediaIds?: string[]
tags?: string[]
}
function businessPath(suffix = '') {
const businessId = getActiveBusinessId()
if (!businessId) {
throw new Error('No active business selected. Please sign in again.')
}
return `/businesses/${businessId}/products${suffix}`
}
export function mapProductApiToUi(product: ProductApi): Product {
return {
id: product.id,
nameEn: product.title,
nameFa: product.nameFa,
categoryId: product.categoryId ?? '',
category: product.categoryName,
categoryFa: product.categoryNameFa || '',
summary: product.summary,
description: product.descriptionHtml,
image: product.image || product.thumbnail || '',
thumbnail: product.thumbnail || product.thumbnailUrl || '',
images: getProductGalleryUrls(product),
tags: product.tags,
status: product.status === 'published' ? 'active' : 'draft',
commentCount: product.commentCount,
variantCount: product.variantCount ?? 0,
}
}
export function getProductGalleryUrls(product: ProductApi): string[] {
const urls: string[] = []
const featured = product.thumbnailUrl || product.thumbnail || product.image
if (featured) {
urls.push(featured)
}
for (const item of product.images) {
if (item.url && !urls.includes(item.url)) {
urls.push(item.url)
}
}
return urls
}
export function mapProductApiToFormState(product: ProductApi) {
return {
categoryId: product.categoryId ?? '',
brandId: product.brandId ?? '',
nameFa: product.nameFa,
nameEn: product.title,
summary: product.summary,
description: product.descriptionHtml,
thumbnail: product.thumbnailUrl ?? product.thumbnail ?? null,
images: product.images.map((item) => item.url),
tags: product.tags,
thumbnailMediaId: product.thumbnailMediaId,
galleryMediaIds: product.galleryMediaIds,
}
}
export async function listProducts(
page = 1,
pageSize = PRODUCTS_PER_PAGE,
signal?: AbortSignal,
filters?: { name?: string },
) {
const params = new URLSearchParams({
page: String(page),
pageSize: String(pageSize),
})
if (filters?.name?.trim()) {
params.set('name', filters.name.trim())
}
const data = await apiRequest<ProductsListResponse>(
`${businessPath()}?${params.toString()}`,
{ auth: true, signal },
)
return {
...data,
items: data.items.map(mapProductApiToUi),
}
}
export async function listAllProducts(signal?: AbortSignal) {
const pageSize = 100
let page = 1
let items: ProductApi[] = []
let total = Infinity
while (items.length < total) {
const data = await apiRequest<ProductsListResponse>(
`${businessPath()}?page=${page}&pageSize=${pageSize}`,
{ auth: true, signal },
)
items = items.concat(data.items)
total = data.total
page += 1
}
return items
}
export async function getProduct(productId: string, signal?: AbortSignal) {
const data = await apiRequest<{ product: ProductApi }>(
businessPath(`/${productId}`),
{ auth: true, signal },
)
return data.product
}
export async function createProduct(payload: CreateProductPayload) {
return apiRequest<{ message: string; product: ProductApi }>(businessPath(), {
method: 'POST',
auth: true,
body: payload,
})
}
export async function updateProduct(productId: string, payload: UpdateProductPayload) {
return apiRequest<{ message: string; product: ProductApi }>(
businessPath(`/${productId}`),
{
method: 'PATCH',
auth: true,
body: payload,
},
)
}
export async function deleteProduct(productId: string) {
return apiRequest<{ message: string }>(businessPath(`/${productId}`), {
method: 'DELETE',
auth: true,
})
}