Initial commit: Meshkee dashboards monorepo.
Includes business, customer, and super-admin apps with shared packages and production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { apiRequest, setTokens, clearTokens } from '../lib/api'
|
||||
import { clearActiveBusiness } from '../lib/businessContext'
|
||||
import type {
|
||||
LoginResponse,
|
||||
MeResponse,
|
||||
OtpSendResponse,
|
||||
OtpVerifyResponse,
|
||||
RegisterResponse,
|
||||
} from '../types/auth'
|
||||
|
||||
export async function login(cellNumber: string, password: string) {
|
||||
const data = await apiRequest<LoginResponse>('/auth/login', {
|
||||
method: 'POST',
|
||||
body: { cellNumber, password },
|
||||
})
|
||||
|
||||
setTokens(data.accessToken, data.refreshToken)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function register(input: {
|
||||
cellNumber: string
|
||||
password: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
email?: string
|
||||
domain: string
|
||||
}) {
|
||||
const data = await apiRequest<RegisterResponse>('/auth/register', {
|
||||
method: 'POST',
|
||||
body: input,
|
||||
})
|
||||
|
||||
setTokens(data.accessToken, data.refreshToken)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchCurrentUser(signal?: AbortSignal) {
|
||||
return apiRequest<MeResponse>('/auth/me', { auth: true, signal })
|
||||
}
|
||||
|
||||
export async function sendOtp(cellNumber: string) {
|
||||
return apiRequest<OtpSendResponse>('/auth/send-otp', {
|
||||
method: 'POST',
|
||||
body: { cellNumber },
|
||||
})
|
||||
}
|
||||
|
||||
export async function verifyOtp(cellNumber: string, code: string) {
|
||||
return apiRequest<OtpVerifyResponse>('/auth/verify-otp', {
|
||||
method: 'POST',
|
||||
body: { cellNumber, code },
|
||||
})
|
||||
}
|
||||
|
||||
export async function changePassword(currentPassword: string, newPassword: string) {
|
||||
return apiRequest<{ message: string }>('/auth/change-password', {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: { currentPassword, newPassword },
|
||||
})
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
clearTokens()
|
||||
clearActiveBusiness()
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type {
|
||||
CategoryFormData,
|
||||
CreateCategoryPayload,
|
||||
ProductCategoriesResponse,
|
||||
ProductCategory,
|
||||
UpdateCategoryPayload,
|
||||
} from '../types/category'
|
||||
|
||||
const ENTITY_TYPE = 'blog' as const
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/categories${suffix}`
|
||||
}
|
||||
|
||||
export async function listBlogCategories(signal?: AbortSignal) {
|
||||
const data = await apiRequest<ProductCategoriesResponse>(
|
||||
`${businessPath()}?entityType=${ENTITY_TYPE}`,
|
||||
{ auth: true, signal },
|
||||
)
|
||||
return data.items
|
||||
}
|
||||
|
||||
export async function createBlogCategory(payload: CreateCategoryPayload) {
|
||||
return apiRequest<{ message: string; category: ProductCategory }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: { ...payload, entityType: ENTITY_TYPE },
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateBlogCategory(categoryId: string, payload: UpdateCategoryPayload) {
|
||||
return apiRequest<{ message: string; category: ProductCategory }>(
|
||||
businessPath(`/${categoryId}`),
|
||||
{
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function deleteBlogCategory(categoryId: string) {
|
||||
return apiRequest<{ message: string; deletedIds: string[] }>(
|
||||
businessPath(`/${categoryId}`),
|
||||
{
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function toBlogCategoryFormPayload(data: CategoryFormData): CreateCategoryPayload {
|
||||
return {
|
||||
parentId: data.parentId || undefined,
|
||||
name: data.nameEn,
|
||||
nameFa: data.nameFa,
|
||||
description: data.description || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapBlogCategoryToUi(category: ProductCategory) {
|
||||
return {
|
||||
id: category.id,
|
||||
nameEn: category.name,
|
||||
nameFa: category.nameFa ?? '',
|
||||
description: category.description ?? '',
|
||||
parentId: category.parentId,
|
||||
variationCount: category.variationCount ?? 0,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import { extractFirstImageUrl } from '../utils/htmlImages'
|
||||
import type { Blog, BlogAuthor, BlogDetail, BlogPostType, BlogStatus } from '../types/blog'
|
||||
|
||||
export const BLOGS_PER_PAGE = 12
|
||||
|
||||
export interface BlogApi {
|
||||
id: string
|
||||
businessId: string
|
||||
title: string
|
||||
slug: string
|
||||
type: BlogPostType
|
||||
abstract: string
|
||||
mainTextHtml: string
|
||||
status: BlogStatus
|
||||
categoryId: string | null
|
||||
categoryName: string
|
||||
tags: string[]
|
||||
titleImageUrl: string | null
|
||||
featuredMediaId: string | null
|
||||
authorId?: string | null
|
||||
author?: BlogAuthor | null
|
||||
commentCount: number
|
||||
publishedAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface BlogsListResponse {
|
||||
items: BlogApi[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface ListBlogsParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: BlogStatus
|
||||
type?: BlogPostType
|
||||
categoryId?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/blogs${suffix}`
|
||||
}
|
||||
|
||||
export interface CreateBlogPayload {
|
||||
title: string
|
||||
type: BlogPostType
|
||||
abstract?: string
|
||||
mainTextHtml?: string
|
||||
categoryId?: string
|
||||
status?: BlogStatus
|
||||
featuredMediaId?: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export interface UpdateBlogPayload {
|
||||
title?: string
|
||||
type?: BlogPostType
|
||||
abstract?: string | null
|
||||
mainTextHtml?: string | null
|
||||
categoryId?: string | null
|
||||
status?: BlogStatus
|
||||
featuredMediaId?: string | null
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export function resolveBlogTitleImageUrl(blog: BlogApi): string | null {
|
||||
return blog.titleImageUrl?.trim() || extractFirstImageUrl(blog.mainTextHtml) || null
|
||||
}
|
||||
|
||||
export function mapBlogApiToUi(blog: BlogApi): Blog {
|
||||
return {
|
||||
id: blog.id,
|
||||
title: blog.title,
|
||||
slug: blog.slug,
|
||||
type: blog.type,
|
||||
abstract: blog.abstract,
|
||||
status: blog.status,
|
||||
categoryId: blog.categoryId,
|
||||
categoryName: blog.categoryName,
|
||||
tags: blog.tags,
|
||||
titleImageUrl: resolveBlogTitleImageUrl(blog),
|
||||
commentCount: blog.commentCount,
|
||||
author: blog.author ?? null,
|
||||
publishedAt: blog.publishedAt,
|
||||
createdAt: blog.createdAt,
|
||||
updatedAt: blog.updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapBlogDetailFromApi(blog: BlogApi): BlogDetail {
|
||||
return {
|
||||
...mapBlogApiToUi(blog),
|
||||
mainTextHtml: blog.mainTextHtml,
|
||||
author: blog.author ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export function formatBlogAuthor(author: BlogAuthor | null): string {
|
||||
if (!author) return 'Unknown author'
|
||||
const name = [author.firstName, author.lastName].filter(Boolean).join(' ').trim()
|
||||
return name || author.email || 'Unknown author'
|
||||
}
|
||||
|
||||
export function formatBlogDate(value: string | null): string {
|
||||
if (!value) return '—'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export function formatBlogCardDate(value: string | null): string {
|
||||
if (!value) return 'Not published'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export function isBlogVerified(blog: Pick<Blog, 'status'>): boolean {
|
||||
return blog.status === 'published'
|
||||
}
|
||||
|
||||
export async function setBlogVerified(blogId: string, verified: boolean) {
|
||||
return updateBlog(blogId, { status: verified ? 'published' : 'draft' })
|
||||
}
|
||||
|
||||
export function mapBlogApiToFormState(blog: BlogApi) {
|
||||
return {
|
||||
title: blog.title,
|
||||
type: blog.type,
|
||||
abstract: blog.abstract,
|
||||
mainTextHtml: blog.mainTextHtml,
|
||||
categoryId: blog.categoryId ?? '',
|
||||
tags: blog.tags,
|
||||
titleImage: resolveBlogTitleImageUrl(blog),
|
||||
featuredMediaId: blog.featuredMediaId,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listBlogs(
|
||||
page = 1,
|
||||
pageSize = BLOGS_PER_PAGE,
|
||||
signal?: AbortSignal,
|
||||
filters?: Omit<ListBlogsParams, 'page' | 'pageSize'>,
|
||||
) {
|
||||
const q = new URLSearchParams()
|
||||
q.set('page', String(page))
|
||||
q.set('pageSize', String(pageSize))
|
||||
if (filters?.status) q.set('status', filters.status)
|
||||
if (filters?.type) q.set('type', filters.type)
|
||||
if (filters?.categoryId) q.set('categoryId', filters.categoryId)
|
||||
if (filters?.title) q.set('title', filters.title)
|
||||
|
||||
const data = await apiRequest<BlogsListResponse>(`${businessPath()}?${q.toString()}`, {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
|
||||
return {
|
||||
...data,
|
||||
items: data.items.map(mapBlogApiToUi),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBlog(blogId: string, signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ blog: BlogApi }>(businessPath(`/${blogId}`), {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
return data.blog
|
||||
}
|
||||
|
||||
export async function getBlogDetail(blogId: string, signal?: AbortSignal) {
|
||||
const blog = await getBlog(blogId, signal)
|
||||
return mapBlogDetailFromApi(blog)
|
||||
}
|
||||
|
||||
export async function createBlog(payload: CreateBlogPayload) {
|
||||
return apiRequest<{ message: string; blog: BlogApi }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateBlog(blogId: string, payload: UpdateBlogPayload) {
|
||||
return apiRequest<{ message: string; blog: BlogApi }>(businessPath(`/${blogId}`), {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteBlog(blogId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${blogId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type {
|
||||
BrandApi,
|
||||
BrandFormData,
|
||||
BrandsListResponse,
|
||||
CreateBrandPayload,
|
||||
UpdateBrandPayload,
|
||||
} from '../types/brand'
|
||||
import type { FlatCategory } from '../types/category'
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/brands${suffix}`
|
||||
}
|
||||
|
||||
export async function listBrands(
|
||||
page = 1,
|
||||
pageSize = 100,
|
||||
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())
|
||||
}
|
||||
|
||||
return apiRequest<BrandsListResponse>(`${businessPath()}?${params.toString()}`, {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
}
|
||||
|
||||
export async function listAllBrands(signal?: AbortSignal) {
|
||||
const pageSize = 100
|
||||
let page = 1
|
||||
let items: BrandApi[] = []
|
||||
let total = Infinity
|
||||
|
||||
while (items.length < total) {
|
||||
const data = await listBrands(page, pageSize, signal)
|
||||
items = items.concat(data.items)
|
||||
total = data.total
|
||||
page += 1
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
export async function createBrand(payload: CreateBrandPayload) {
|
||||
return apiRequest<{ message: string; brand: BrandApi }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateBrand(brandId: string, payload: UpdateBrandPayload) {
|
||||
return apiRequest<{ message: string; brand: BrandApi }>(businessPath(`/${brandId}`), {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteBrand(brandId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${brandId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
export function toBrandFormPayload(
|
||||
data: BrandFormData,
|
||||
imageMediaId?: string | null,
|
||||
): CreateBrandPayload {
|
||||
return {
|
||||
nameEn: data.nameEn,
|
||||
nameFa: data.nameFa || undefined,
|
||||
about: data.about || undefined,
|
||||
...(imageMediaId ? { imageMediaId } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
export function mapBrandApiToUi(brand: BrandApi) {
|
||||
return {
|
||||
id: brand.id,
|
||||
nameEn: brand.nameEn,
|
||||
nameFa: brand.nameFa ?? '',
|
||||
about: brand.about ?? '',
|
||||
slug: brand.slug,
|
||||
imageUrl: brand.imageUrl,
|
||||
imageMediaId: brand.imageMediaId,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapBrandToSelectOption(brand: BrandApi): FlatCategory {
|
||||
return {
|
||||
id: brand.id,
|
||||
nameEn: brand.nameEn,
|
||||
nameFa: brand.nameFa ?? '',
|
||||
description: brand.about ?? '',
|
||||
parentId: null,
|
||||
variationCount: 0,
|
||||
depth: 0,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
|
||||
export interface BusinessActivityCategory {
|
||||
id: string
|
||||
parentId: string | null
|
||||
name: string
|
||||
slug: string
|
||||
description: string | null
|
||||
icon: string | null
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
export async function listBusinessActivityCategories(signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ items: BusinessActivityCategory[] }>(
|
||||
'/business-categories',
|
||||
{ auth: true, signal },
|
||||
)
|
||||
return data.items.map((item) => ({
|
||||
...item,
|
||||
id: String(item.id),
|
||||
parentId: item.parentId ? String(item.parentId) : null,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
|
||||
export type PhoneNumberType = 'landline' | 'cell'
|
||||
|
||||
export interface BusinessPhoneNumber {
|
||||
type: PhoneNumberType
|
||||
number: string
|
||||
}
|
||||
|
||||
export interface BusinessSocialMedia {
|
||||
whatsapp: string
|
||||
telegram: string
|
||||
instagram: string
|
||||
linkedin: string
|
||||
youtube: string
|
||||
aparat: string
|
||||
}
|
||||
|
||||
export interface BusinessProfile {
|
||||
nameEn: string
|
||||
nameFa: string
|
||||
about: string
|
||||
vision: string
|
||||
emails: string[]
|
||||
phoneNumbers: BusinessPhoneNumber[]
|
||||
socialMedia: BusinessSocialMedia
|
||||
logoMediaId: string | null
|
||||
logoUrl: string | null
|
||||
faviconMediaId: string | null
|
||||
faviconUrl: string | null
|
||||
categoryIds: string[]
|
||||
}
|
||||
|
||||
export interface BusinessAddress {
|
||||
id: string
|
||||
province: string
|
||||
city: string
|
||||
address: string
|
||||
postalCode: string
|
||||
landline: string | null
|
||||
}
|
||||
|
||||
export interface BusinessProfileResponse {
|
||||
businessId: string
|
||||
profile: BusinessProfile
|
||||
addresses: BusinessAddress[]
|
||||
}
|
||||
|
||||
export interface UpdateBusinessProfilePayload {
|
||||
nameEn?: string
|
||||
nameFa?: string
|
||||
about?: string
|
||||
vision?: string
|
||||
emails?: string[]
|
||||
phoneNumbers?: BusinessPhoneNumber[]
|
||||
socialMedia?: Partial<BusinessSocialMedia>
|
||||
logoMediaId?: string | null
|
||||
categoryIds?: string[]
|
||||
addresses?: Array<{
|
||||
id?: string
|
||||
province: string
|
||||
city: string
|
||||
address: string
|
||||
postalCode: string
|
||||
landline?: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
function profilePath() {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/profile`
|
||||
}
|
||||
|
||||
export async function getBusinessProfile(signal?: AbortSignal) {
|
||||
return apiRequest<BusinessProfileResponse>(profilePath(), { auth: true, signal })
|
||||
}
|
||||
|
||||
export async function updateBusinessProfile(payload: UpdateBusinessProfilePayload) {
|
||||
return apiRequest<BusinessProfileResponse>(profilePath(), {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: {
|
||||
...payload,
|
||||
categoryIds: payload.categoryIds?.map((id) => Number(id)),
|
||||
logoMediaId:
|
||||
payload.logoMediaId === undefined
|
||||
? undefined
|
||||
: payload.logoMediaId
|
||||
? Number(payload.logoMediaId)
|
||||
: null,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type {
|
||||
CategoryTechnicalForm,
|
||||
TechnicalFormFieldDraft,
|
||||
TechnicalFormField,
|
||||
} from '../types/technicalForm'
|
||||
|
||||
function businessPath(categoryId: string) {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/categories/${categoryId}/technical-form`
|
||||
}
|
||||
|
||||
export function mapApiFieldToDraft(field: TechnicalFormField): TechnicalFormFieldDraft {
|
||||
return {
|
||||
id: field.id,
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
isRequired: field.isRequired,
|
||||
options: field.options.map((option) => option.label),
|
||||
}
|
||||
}
|
||||
|
||||
export function mapDraftToPayload(fields: TechnicalFormFieldDraft[]) {
|
||||
return {
|
||||
fields: fields.map((field) => ({
|
||||
label: field.label.trim(),
|
||||
type: field.type,
|
||||
isRequired: field.isRequired,
|
||||
...(field.type === 'select' || field.type === 'multi_select'
|
||||
? { options: field.options.map((option) => option.trim()).filter(Boolean) }
|
||||
: {}),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCategoryTechnicalForm(categoryId: string, signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ form: CategoryTechnicalForm | null }>(
|
||||
businessPath(categoryId),
|
||||
{ auth: true, signal },
|
||||
)
|
||||
return data.form
|
||||
}
|
||||
|
||||
export async function saveCategoryTechnicalForm(
|
||||
categoryId: string,
|
||||
fields: TechnicalFormFieldDraft[],
|
||||
) {
|
||||
const data = await apiRequest<{ form: CategoryTechnicalForm | null }>(
|
||||
businessPath(categoryId),
|
||||
{
|
||||
method: 'PUT',
|
||||
auth: true,
|
||||
body: mapDraftToPayload(fields),
|
||||
},
|
||||
)
|
||||
return data.form
|
||||
}
|
||||
|
||||
export async function suggestCategoryTechnicalFormByAi(
|
||||
categoryId: string,
|
||||
hint?: string,
|
||||
) {
|
||||
const data = await apiRequest<{
|
||||
message: string
|
||||
fields: {
|
||||
label: string
|
||||
type: TechnicalFormFieldDraft['type']
|
||||
isRequired: boolean
|
||||
options?: string[]
|
||||
}[]
|
||||
}>(`${businessPath(categoryId)}/ai-suggest`, {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: hint ? { hint } : {},
|
||||
})
|
||||
|
||||
return data.fields
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type { Variation, VariationType } from '../types/variation'
|
||||
|
||||
export interface CategoryVariationApi {
|
||||
id: string
|
||||
name: string
|
||||
type: VariationType
|
||||
sortOrder: number
|
||||
values: string[]
|
||||
options: {
|
||||
id: string
|
||||
label: string
|
||||
value: string
|
||||
colorHex: string | null
|
||||
sortOrder: number
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface ColorPreset {
|
||||
name: string
|
||||
hex: string
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/categories${suffix}`
|
||||
}
|
||||
|
||||
export function mapApiVariationToUi(item: CategoryVariationApi): Variation {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
values: item.values,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listColorPresets(signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ items: ColorPreset[] }>(businessPath('/color-presets'), {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
return data.items
|
||||
}
|
||||
|
||||
export async function listCategoryVariations(categoryId: string, signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ items: CategoryVariationApi[] }>(
|
||||
businessPath(`/${categoryId}/variations`),
|
||||
{ auth: true, signal },
|
||||
)
|
||||
return data.items.map(mapApiVariationToUi)
|
||||
}
|
||||
|
||||
export async function fetchCategoryVariations(categoryId: string, signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ items: CategoryVariationApi[] }>(
|
||||
businessPath(`/${categoryId}/variations`),
|
||||
{ auth: true, signal },
|
||||
)
|
||||
return data.items
|
||||
}
|
||||
|
||||
export async function saveCategoryVariations(
|
||||
categoryId: string,
|
||||
variations: Variation[],
|
||||
) {
|
||||
const data = await apiRequest<{ items: CategoryVariationApi[] }>(
|
||||
businessPath(`/${categoryId}/variations`),
|
||||
{
|
||||
method: 'PUT',
|
||||
auth: true,
|
||||
body: {
|
||||
variations: variations.map((variation) => ({
|
||||
type: variation.type,
|
||||
name: variation.name,
|
||||
values: variation.values,
|
||||
})),
|
||||
},
|
||||
},
|
||||
)
|
||||
return data.items.map(mapApiVariationToUi)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import type { CityOption } from '@meshkee/dashboard-ui'
|
||||
|
||||
export type { CityOption }
|
||||
|
||||
export async function listIranProvinces(signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ items: CityOption[] }>(
|
||||
'/cities?level=province&parentSlug=iran',
|
||||
{ signal },
|
||||
)
|
||||
return data.items
|
||||
}
|
||||
|
||||
export async function listCitiesByProvinceSlug(parentSlug: string, signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ items: CityOption[] }>(
|
||||
`/cities?level=city&parentSlug=${encodeURIComponent(parentSlug)}`,
|
||||
{ signal },
|
||||
)
|
||||
return data.items
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type { ProductComment } from '../types/comment'
|
||||
|
||||
export const COMMENTS_PER_PAGE = 12
|
||||
|
||||
export interface CommentApi {
|
||||
id: string
|
||||
businessId: string
|
||||
entityType: string
|
||||
entityId: string
|
||||
authorName: string
|
||||
authorEmail: string | null
|
||||
text: string
|
||||
isApproved: boolean
|
||||
approvedAt: string | null
|
||||
approvedBy: string | null
|
||||
approver: { id: string; firstName: string; lastName: string } | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface CommentsListResponse {
|
||||
items: CommentApi[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
function commentsPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/comments${suffix}`
|
||||
}
|
||||
|
||||
export function mapCommentApiToUi(comment: CommentApi): ProductComment {
|
||||
return {
|
||||
id: comment.id,
|
||||
author: comment.authorName,
|
||||
createdAt: comment.createdAt,
|
||||
text: comment.text,
|
||||
likesCount: 0,
|
||||
approved: comment.isApproved,
|
||||
}
|
||||
}
|
||||
|
||||
async function listEntityComments(
|
||||
entityType: 'product' | 'blog' | 'portfolio',
|
||||
entityId: string,
|
||||
page = 1,
|
||||
pageSize = COMMENTS_PER_PAGE,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
entityType,
|
||||
entityId,
|
||||
page: String(page),
|
||||
pageSize: String(pageSize),
|
||||
})
|
||||
|
||||
const data = await apiRequest<CommentsListResponse>(`${commentsPath()}?${params}`, {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
|
||||
return {
|
||||
...data,
|
||||
items: data.items.map(mapCommentApiToUi),
|
||||
}
|
||||
}
|
||||
|
||||
export async function listProductComments(
|
||||
productId: string,
|
||||
page = 1,
|
||||
pageSize = COMMENTS_PER_PAGE,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
return listEntityComments('product', productId, page, pageSize, signal)
|
||||
}
|
||||
|
||||
export async function listBlogComments(
|
||||
blogId: string,
|
||||
page = 1,
|
||||
pageSize = COMMENTS_PER_PAGE,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
return listEntityComments('blog', blogId, page, pageSize, signal)
|
||||
}
|
||||
|
||||
export async function listPortfolioComments(
|
||||
portfolioId: string,
|
||||
page = 1,
|
||||
pageSize = COMMENTS_PER_PAGE,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
return listEntityComments('portfolio', portfolioId, page, pageSize, signal)
|
||||
}
|
||||
|
||||
export async function updateCommentApproval(commentId: string, isApproved: boolean) {
|
||||
const data = await apiRequest<{ comment: CommentApi }>(commentsPath(`/${commentId}`), {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: { isApproved },
|
||||
})
|
||||
return mapCommentApiToUi(data.comment)
|
||||
}
|
||||
|
||||
export async function deleteComment(commentId: string) {
|
||||
return apiRequest<{ message: string }>(commentsPath(`/${commentId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type {
|
||||
ContactSubmission,
|
||||
ContactSubmissionsListResponse,
|
||||
} from '../types/contactSubmission'
|
||||
|
||||
export interface ListContactSubmissionsParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
q?: string
|
||||
}
|
||||
|
||||
function basePath() {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/contact-submissions`
|
||||
}
|
||||
|
||||
export async function listContactSubmissions(
|
||||
params: ListContactSubmissionsParams,
|
||||
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.q) q.set('q', params.q)
|
||||
|
||||
const query = q.toString()
|
||||
const path = `${basePath()}${query ? `?${query}` : ''}`
|
||||
|
||||
return apiRequest<ContactSubmissionsListResponse>(path, { auth: true, signal })
|
||||
}
|
||||
|
||||
export async function getContactSubmission(submissionId: string, signal?: AbortSignal) {
|
||||
return apiRequest<{ submission: ContactSubmission }>(`${basePath()}/${submissionId}`, {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
|
||||
export interface BusinessCustomer {
|
||||
id: string
|
||||
cellNumber: string
|
||||
firstName: string | null
|
||||
lastName: string | null
|
||||
email: string | null
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface BusinessCustomerListItem extends BusinessCustomer {
|
||||
createdAt: string
|
||||
isEnabled: boolean
|
||||
/** Placeholder until orders stats API exists */
|
||||
orderCount?: number | null
|
||||
/** Placeholder until orders stats API exists */
|
||||
totalTransactionsIrt?: number | null
|
||||
}
|
||||
|
||||
export interface CustomersListResponse {
|
||||
items: BusinessCustomerListItem[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface ListCustomersParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
name?: string
|
||||
cellNumber?: string
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/customers${suffix}`
|
||||
}
|
||||
|
||||
export async function listCustomers(
|
||||
params: ListCustomersParams = {},
|
||||
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.name) q.set('name', params.name)
|
||||
if (params.cellNumber) q.set('cellNumber', params.cellNumber)
|
||||
|
||||
const query = q.toString()
|
||||
const path = `${businessPath()}${query ? `?${query}` : ''}`
|
||||
return apiRequest<CustomersListResponse>(path, { auth: true, signal })
|
||||
}
|
||||
|
||||
export interface UpdateCustomerPayload {
|
||||
isEnabled?: boolean
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
email?: string
|
||||
cellNumber?: string
|
||||
}
|
||||
|
||||
export interface CreateCustomerPayload {
|
||||
cellNumber: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
password?: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
export async function createCustomer(payload: CreateCustomerPayload) {
|
||||
return apiRequest<BusinessCustomerListItem & { isVerified: boolean; role: string }>(
|
||||
businessPath(),
|
||||
{
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function updateCustomer(userId: string, payload: UpdateCustomerPayload) {
|
||||
return apiRequest<BusinessCustomer & { isEnabled: boolean }>(`${businessPath(`/${userId}`)}`, {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateCustomerEnabled(userId: string, isEnabled: boolean) {
|
||||
return updateCustomer(userId, { isEnabled })
|
||||
}
|
||||
|
||||
export async function removeCustomer(userId: string) {
|
||||
return apiRequest<{ success: boolean }>(`${businessPath(`/${userId}`)}`, {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
export async function searchCustomers(
|
||||
query: string,
|
||||
limit = 20,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const q = query.trim()
|
||||
if (q.length < 2) {
|
||||
return { items: [] as BusinessCustomer[] }
|
||||
}
|
||||
|
||||
return apiRequest<{ items: BusinessCustomer[] }>(
|
||||
`${businessPath('/search')}?q=${encodeURIComponent(q)}&limit=${limit}`,
|
||||
{ auth: true, signal },
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type { ProductExpertReview } from '../types/expertReview'
|
||||
|
||||
export const EXPERT_REVIEWS_PER_PAGE = 20
|
||||
|
||||
export interface ExpertReviewApi {
|
||||
id: string
|
||||
businessId: string
|
||||
productId: string
|
||||
authorName: string
|
||||
authorEmail: string | null
|
||||
rate: number
|
||||
positivePoints: string[]
|
||||
negativePoints: string[]
|
||||
text: string
|
||||
isApproved: boolean
|
||||
approvedAt: string | null
|
||||
approvedBy: string | null
|
||||
approver: { id: string; firstName: string; lastName: string } | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface ExpertReviewsListResponse {
|
||||
items: ExpertReviewApi[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
function expertReviewsPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/expert-reviews${suffix}`
|
||||
}
|
||||
|
||||
export function mapExpertReviewApiToUi(review: ExpertReviewApi): ProductExpertReview {
|
||||
return {
|
||||
id: review.id,
|
||||
authorName: review.authorName,
|
||||
rate: review.rate,
|
||||
positivePoints: review.positivePoints ?? [],
|
||||
negativePoints: review.negativePoints ?? [],
|
||||
text: review.text,
|
||||
approved: review.isApproved,
|
||||
createdAt: review.createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
export function formatReviewDate(iso: string): string {
|
||||
return new Date(iso).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export async function listProductExpertReviews(
|
||||
productId: string,
|
||||
page = 1,
|
||||
pageSize = EXPERT_REVIEWS_PER_PAGE,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
productId,
|
||||
page: String(page),
|
||||
pageSize: String(pageSize),
|
||||
})
|
||||
|
||||
const data = await apiRequest<ExpertReviewsListResponse>(`${expertReviewsPath()}?${params}`, {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
|
||||
return {
|
||||
...data,
|
||||
items: data.items.map(mapExpertReviewApiToUi),
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateExpertReviewApproval(reviewId: string, isApproved: boolean) {
|
||||
const data = await apiRequest<{ review: ExpertReviewApi }>(expertReviewsPath(`/${reviewId}`), {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: { isApproved },
|
||||
})
|
||||
return mapExpertReviewApiToUi(data.review)
|
||||
}
|
||||
|
||||
export async function deleteExpertReview(reviewId: string) {
|
||||
return apiRequest<{ success: boolean }>(expertReviewsPath(`/${reviewId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { getAccessToken, clearTokens, setTokens } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import { API_BASE_URL } from '../lib/config'
|
||||
import { ensureJpegUploadFile, ensureUploadFile } from '../utils/imageUpload'
|
||||
|
||||
export interface MediaItem {
|
||||
id: string
|
||||
publicUrl: string
|
||||
fileName: string
|
||||
originalFileName: string
|
||||
mimeType: string
|
||||
width: number | null
|
||||
height: number | null
|
||||
}
|
||||
|
||||
function isDataUrl(value: string) {
|
||||
return value.startsWith('data:')
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/media${suffix}`
|
||||
}
|
||||
|
||||
async function refreshAccessToken() {
|
||||
const refreshToken = localStorage.getItem('meshkee_business_refresh_token')
|
||||
if (!refreshToken) return false
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
clearTokens()
|
||||
return false
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setTokens(data.accessToken, data.refreshToken)
|
||||
return true
|
||||
}
|
||||
|
||||
function parseUploadError(payload: unknown, status: number) {
|
||||
if (payload && typeof payload === 'object') {
|
||||
const message = (payload as { message?: string | string[] }).message
|
||||
if (Array.isArray(message)) {
|
||||
return message.join(', ')
|
||||
}
|
||||
if (typeof message === 'string' && message) {
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
return `Upload failed with status ${status}`
|
||||
}
|
||||
|
||||
export async function uploadMediaFiles(
|
||||
files: File[],
|
||||
signal?: AbortSignal,
|
||||
): Promise<MediaItem[]> {
|
||||
if (!files.length) return []
|
||||
|
||||
const send = async () => {
|
||||
const formData = new FormData()
|
||||
files.forEach((file) => formData.append('files', file))
|
||||
|
||||
const headers = new Headers()
|
||||
const accessToken = getAccessToken()
|
||||
if (accessToken) {
|
||||
headers.set('Authorization', `Bearer ${accessToken}`)
|
||||
}
|
||||
|
||||
return fetch(`${API_BASE_URL}${businessPath()}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
signal,
|
||||
})
|
||||
}
|
||||
|
||||
let response = await send()
|
||||
|
||||
if (response.status === 401) {
|
||||
const refreshed = await refreshAccessToken()
|
||||
if (refreshed) {
|
||||
response = await send()
|
||||
}
|
||||
}
|
||||
|
||||
const payload = await response.json().catch(() => null)
|
||||
if (!response.ok) {
|
||||
throw new Error(parseUploadError(payload, response.status))
|
||||
}
|
||||
|
||||
return (payload.items as Array<Record<string, unknown>>).map((item) => ({
|
||||
id: String(item.id),
|
||||
publicUrl: String(item.publicUrl),
|
||||
fileName: String(item.fileName),
|
||||
originalFileName: String(item.originalFileName),
|
||||
mimeType: String(item.mimeType),
|
||||
width: typeof item.width === 'number' ? item.width : null,
|
||||
height: typeof item.height === 'number' ? item.height : null,
|
||||
}))
|
||||
}
|
||||
|
||||
export async function resolveDataUrlToMediaId(
|
||||
value: string | null,
|
||||
filename: string,
|
||||
existingMediaId?: string | null,
|
||||
): Promise<string | null> {
|
||||
if (!value) return null
|
||||
if (!isDataUrl(value)) {
|
||||
return existingMediaId ?? null
|
||||
}
|
||||
|
||||
const file = filename.toLowerCase().endsWith('.png')
|
||||
? await ensureUploadFile(value, filename)
|
||||
: await ensureJpegUploadFile(value, filename)
|
||||
const uploaded = await uploadMediaFiles([file])
|
||||
return uploaded[0]?.id ?? null
|
||||
}
|
||||
|
||||
export async function resolveHtmlEmbeddedDataUrls(html: string): Promise<string> {
|
||||
if (!html.includes('data:')) return html
|
||||
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(html, 'text/html')
|
||||
const images = Array.from(doc.querySelectorAll('img'))
|
||||
|
||||
for (let index = 0; index < images.length; index += 1) {
|
||||
const src = images[index].getAttribute('src')
|
||||
if (!src || !isDataUrl(src)) continue
|
||||
|
||||
const file = await ensureJpegUploadFile(src, `inline-image-${index + 1}.jpg`)
|
||||
const uploaded = await uploadMediaFiles([file])
|
||||
if (uploaded[0]) {
|
||||
images[index].setAttribute('src', uploaded[0].publicUrl)
|
||||
}
|
||||
}
|
||||
|
||||
return doc.body.innerHTML
|
||||
}
|
||||
|
||||
export async function resolveDataUrlsToMediaIds(
|
||||
values: string[],
|
||||
existingMediaIds: string[],
|
||||
): Promise<string[]> {
|
||||
const resolved: string[] = []
|
||||
|
||||
for (let index = 0; index < values.length; index += 1) {
|
||||
const value = values[index]
|
||||
if (isDataUrl(value)) {
|
||||
const file = await ensureJpegUploadFile(value, `product-image-${index + 1}.jpg`)
|
||||
const uploaded = await uploadMediaFiles([file])
|
||||
if (uploaded[0]) resolved.push(uploaded[0].id)
|
||||
} else if (existingMediaIds[index]) {
|
||||
resolved.push(existingMediaIds[index])
|
||||
}
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
|
||||
export type OrderStatus =
|
||||
| 'pending'
|
||||
| 'confirmed'
|
||||
| 'processing'
|
||||
| 'shipped'
|
||||
| 'delivered'
|
||||
| 'cancelled'
|
||||
|
||||
export type OrderSource = 'website' | 'admin' | 'app'
|
||||
|
||||
export interface OrderItem {
|
||||
id: string
|
||||
storeItemVariantId: string | null
|
||||
productId: string
|
||||
productTitle: string
|
||||
productImage: string | null
|
||||
variantSku: string | null
|
||||
unitPrice: number
|
||||
compareAtPrice: number | null
|
||||
quantity: number
|
||||
lineTotal: number
|
||||
selections: {
|
||||
variationId: string
|
||||
variationName: string
|
||||
optionId: string
|
||||
value: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface OrderCustomer {
|
||||
id: string
|
||||
firstName: string | null
|
||||
lastName: string | null
|
||||
cellNumber: string
|
||||
email: string | null
|
||||
}
|
||||
|
||||
export type TransactionType = 'pos' | 'cash' | 'transfer' | 'e_payment_gate'
|
||||
|
||||
export type TransactionStatus = 'pending' | 'completed' | 'failed' | 'refunded'
|
||||
|
||||
export interface OrderTransaction {
|
||||
id: string
|
||||
type: TransactionType
|
||||
status: TransactionStatus
|
||||
amount: number
|
||||
posType: string | null
|
||||
gatewayType: string | null
|
||||
transferAccount: string | null
|
||||
transferRefNumber: string | null
|
||||
notes: string | null
|
||||
createdBy: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string
|
||||
businessId: string
|
||||
orderNumber: string
|
||||
status: OrderStatus
|
||||
processStepId: string
|
||||
processStepLabel?: string | null
|
||||
processStepColor?: string | null
|
||||
source: OrderSource
|
||||
subtotal: number
|
||||
shippingTotal: number
|
||||
discountTotal: number
|
||||
total: number
|
||||
shippingAddress: Record<string, unknown>
|
||||
addressId: string | null
|
||||
customerNotes: string | null
|
||||
adminNotes: string | null
|
||||
createdBy: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
customer: OrderCustomer
|
||||
items: OrderItem[]
|
||||
transactions?: OrderTransaction[]
|
||||
}
|
||||
|
||||
export interface OrdersListResponse {
|
||||
items: Order[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface ListOrdersParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: OrderStatus
|
||||
orderNumber?: string
|
||||
customerQuery?: string
|
||||
dateFrom?: string
|
||||
dateTo?: string
|
||||
minTotal?: number
|
||||
maxTotal?: number
|
||||
}
|
||||
|
||||
export interface CreateAdminOrderPayment {
|
||||
type: 'pos' | 'cash' | 'transfer' | 'e_payment_gate'
|
||||
amount: number
|
||||
posType?: string
|
||||
gatewayType?: string
|
||||
transferAccount?: string
|
||||
transferRefNumber?: string
|
||||
notes?: string
|
||||
}
|
||||
|
||||
export interface CreateAdminOrderPayload {
|
||||
customerUserId: string
|
||||
items: { storeItemVariantId: string; quantity: number }[]
|
||||
customerNotes?: string
|
||||
adminNotes?: string
|
||||
status?: OrderStatus
|
||||
payments?: CreateAdminOrderPayment[]
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/orders${suffix}`
|
||||
}
|
||||
|
||||
export async function listOrders(params: ListOrdersParams = {}, 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)
|
||||
if (params.orderNumber) q.set('orderNumber', params.orderNumber)
|
||||
if (params.customerQuery) q.set('customerQuery', params.customerQuery)
|
||||
if (params.dateFrom) q.set('dateFrom', params.dateFrom)
|
||||
if (params.dateTo) q.set('dateTo', params.dateTo)
|
||||
if (params.minTotal !== undefined) q.set('minTotal', String(params.minTotal))
|
||||
if (params.maxTotal !== undefined) q.set('maxTotal', String(params.maxTotal))
|
||||
|
||||
const query = q.toString()
|
||||
const path = `${businessPath()}${query ? `?${query}` : ''}`
|
||||
return apiRequest<OrdersListResponse>(path, { auth: true, signal })
|
||||
}
|
||||
|
||||
export async function getOrder(orderId: string, signal?: AbortSignal) {
|
||||
return apiRequest<{ order: Order }>(businessPath(`/${orderId}`), { auth: true, signal })
|
||||
}
|
||||
|
||||
export async function createAdminOrder(payload: CreateAdminOrderPayload) {
|
||||
return apiRequest<{ message: string; order: Order }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function removeOrder(orderId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${orderId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
export interface UpdateOrderPayload {
|
||||
processStepId?: string
|
||||
status?: OrderStatus
|
||||
adminNotes?: string
|
||||
}
|
||||
|
||||
export async function updateOrder(orderId: string, payload: UpdateOrderPayload) {
|
||||
return apiRequest<{ message: string; order: Order }>(businessPath(`/${orderId}`), {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type {
|
||||
CategoryFormData,
|
||||
CreateCategoryPayload,
|
||||
ProductCategoriesResponse,
|
||||
ProductCategory,
|
||||
UpdateCategoryPayload,
|
||||
} from '../types/category'
|
||||
|
||||
const ENTITY_TYPE = 'portfolio' as const
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/categories${suffix}`
|
||||
}
|
||||
|
||||
export async function listPortfolioCategories(signal?: AbortSignal) {
|
||||
const data = await apiRequest<ProductCategoriesResponse>(
|
||||
`${businessPath()}?entityType=${ENTITY_TYPE}`,
|
||||
{ auth: true, signal },
|
||||
)
|
||||
return data.items
|
||||
}
|
||||
|
||||
export async function createPortfolioCategory(payload: CreateCategoryPayload) {
|
||||
return apiRequest<{ message: string; category: ProductCategory }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: { ...payload, entityType: ENTITY_TYPE },
|
||||
})
|
||||
}
|
||||
|
||||
export async function updatePortfolioCategory(categoryId: string, payload: UpdateCategoryPayload) {
|
||||
return apiRequest<{ message: string; category: ProductCategory }>(
|
||||
businessPath(`/${categoryId}`),
|
||||
{
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function deletePortfolioCategory(categoryId: string) {
|
||||
return apiRequest<{ message: string; deletedIds: string[] }>(
|
||||
businessPath(`/${categoryId}`),
|
||||
{
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function toPortfolioCategoryFormPayload(data: CategoryFormData): CreateCategoryPayload {
|
||||
return {
|
||||
parentId: data.parentId || undefined,
|
||||
name: data.nameEn,
|
||||
nameFa: data.nameFa,
|
||||
description: data.description || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapPortfolioCategoryToUi(category: ProductCategory) {
|
||||
return {
|
||||
id: category.id,
|
||||
nameEn: category.name,
|
||||
nameFa: category.nameFa ?? '',
|
||||
description: category.description ?? '',
|
||||
parentId: category.parentId,
|
||||
variationCount: category.variationCount ?? 0,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import { extractFirstImageUrl } from '../utils/htmlImages'
|
||||
import type { Portfolio, PortfolioDetail, PortfolioGalleryItem, PortfolioStatus } from '../types/portfolio'
|
||||
|
||||
export const PORTFOLIOS_PER_PAGE = 12
|
||||
|
||||
export interface PortfolioApi {
|
||||
id: string
|
||||
businessId: string
|
||||
title: string
|
||||
slug: string
|
||||
abstract: string
|
||||
mainTextHtml: string
|
||||
status: PortfolioStatus
|
||||
categoryId: string | null
|
||||
categoryName: string
|
||||
tags: string[]
|
||||
titleImageUrl: string | null
|
||||
featuredMediaId: string | null
|
||||
gallery: PortfolioGalleryItem[]
|
||||
galleryMediaIds: string[]
|
||||
sortOrder: number
|
||||
commentCount: number
|
||||
publishedAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface PortfoliosListResponse {
|
||||
items: PortfolioApi[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface ListPortfoliosParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: PortfolioStatus
|
||||
categoryId?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/portfolios${suffix}`
|
||||
}
|
||||
|
||||
export interface CreatePortfolioPayload {
|
||||
title: string
|
||||
abstract?: string
|
||||
mainTextHtml?: string
|
||||
categoryId?: string
|
||||
status?: PortfolioStatus
|
||||
featuredMediaId?: string
|
||||
galleryMediaIds?: string[]
|
||||
tags?: string[]
|
||||
sortOrder?: number
|
||||
}
|
||||
|
||||
export interface UpdatePortfolioPayload {
|
||||
title?: string
|
||||
abstract?: string | null
|
||||
mainTextHtml?: string | null
|
||||
categoryId?: string | null
|
||||
status?: PortfolioStatus
|
||||
featuredMediaId?: string | null
|
||||
galleryMediaIds?: string[]
|
||||
tags?: string[]
|
||||
sortOrder?: number
|
||||
}
|
||||
|
||||
export function resolvePortfolioTitleImageUrl(portfolio: PortfolioApi): string | null {
|
||||
const featured = portfolio.titleImageUrl?.trim()
|
||||
if (featured) return featured
|
||||
|
||||
const galleryUrl = portfolio.gallery?.map((item) => item.url?.trim()).find(Boolean)
|
||||
if (galleryUrl) return galleryUrl
|
||||
|
||||
return extractFirstImageUrl(portfolio.mainTextHtml) || null
|
||||
}
|
||||
|
||||
export function mapPortfolioApiToUi(portfolio: PortfolioApi): Portfolio {
|
||||
return {
|
||||
id: portfolio.id,
|
||||
title: portfolio.title,
|
||||
slug: portfolio.slug,
|
||||
abstract: portfolio.abstract,
|
||||
status: portfolio.status,
|
||||
categoryId: portfolio.categoryId,
|
||||
categoryName: portfolio.categoryName,
|
||||
tags: portfolio.tags,
|
||||
titleImageUrl: resolvePortfolioTitleImageUrl(portfolio),
|
||||
commentCount: portfolio.commentCount,
|
||||
publishedAt: portfolio.publishedAt,
|
||||
createdAt: portfolio.createdAt,
|
||||
updatedAt: portfolio.updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
export function formatPortfolioCardDate(value: string | null): string {
|
||||
if (!value) return 'Not published'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export function formatPortfolioDate(value: string | null): string {
|
||||
if (!value) return '—'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export function mapPortfolioDetailFromApi(portfolio: PortfolioApi): PortfolioDetail {
|
||||
return {
|
||||
...mapPortfolioApiToUi(portfolio),
|
||||
mainTextHtml: portfolio.mainTextHtml,
|
||||
gallery: portfolio.gallery ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
export function mapPortfolioApiToFormState(portfolio: PortfolioApi) {
|
||||
return {
|
||||
title: portfolio.title,
|
||||
abstract: portfolio.abstract,
|
||||
mainTextHtml: portfolio.mainTextHtml,
|
||||
categoryId: portfolio.categoryId ?? '',
|
||||
tags: portfolio.tags,
|
||||
titleImage: resolvePortfolioTitleImageUrl(portfolio),
|
||||
featuredMediaId: portfolio.featuredMediaId,
|
||||
images: portfolio.gallery.map((item) => item.url),
|
||||
galleryMediaIds: portfolio.galleryMediaIds,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listPortfolios(
|
||||
page = 1,
|
||||
pageSize = PORTFOLIOS_PER_PAGE,
|
||||
signal?: AbortSignal,
|
||||
filters?: Omit<ListPortfoliosParams, 'page' | 'pageSize'>,
|
||||
) {
|
||||
const q = new URLSearchParams()
|
||||
q.set('page', String(page))
|
||||
q.set('pageSize', String(pageSize))
|
||||
if (filters?.status) q.set('status', filters.status)
|
||||
if (filters?.categoryId) q.set('categoryId', filters.categoryId)
|
||||
if (filters?.title) q.set('title', filters.title)
|
||||
|
||||
const data = await apiRequest<PortfoliosListResponse>(`${businessPath()}?${q.toString()}`, {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
|
||||
return {
|
||||
...data,
|
||||
items: data.items.map(mapPortfolioApiToUi),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPortfolio(portfolioId: string, signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ portfolio: PortfolioApi }>(businessPath(`/${portfolioId}`), {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
return data.portfolio
|
||||
}
|
||||
|
||||
export async function getPortfolioDetail(portfolioId: string, signal?: AbortSignal) {
|
||||
const portfolio = await getPortfolio(portfolioId, signal)
|
||||
return mapPortfolioDetailFromApi(portfolio)
|
||||
}
|
||||
|
||||
export async function createPortfolio(payload: CreatePortfolioPayload) {
|
||||
return apiRequest<{ message: string; portfolio: PortfolioApi }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updatePortfolio(portfolioId: string, payload: UpdatePortfolioPayload) {
|
||||
return apiRequest<{ message: string; portfolio: PortfolioApi }>(
|
||||
businessPath(`/${portfolioId}`),
|
||||
{
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function deletePortfolio(portfolioId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${portfolioId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type { ProductApi } from './productService'
|
||||
|
||||
export type ProductAiLanguage = 'en' | 'fa'
|
||||
|
||||
export interface CreateProductByAiPayload {
|
||||
categoryId: string
|
||||
name: string
|
||||
language: ProductAiLanguage
|
||||
}
|
||||
|
||||
export async function createProductByAi(payload: CreateProductByAiPayload) {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
|
||||
return apiRequest<{ message: string; product: ProductApi }>(
|
||||
`/businesses/${businessId}/products/ai-create`,
|
||||
{
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type {
|
||||
CategoryFormData,
|
||||
CreateCategoryPayload,
|
||||
ProductCategoriesResponse,
|
||||
ProductCategory,
|
||||
UpdateCategoryPayload,
|
||||
} from '../types/category'
|
||||
|
||||
const ENTITY_TYPE = 'product' as const
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/categories${suffix}`
|
||||
}
|
||||
|
||||
export async function listProductCategories(signal?: AbortSignal) {
|
||||
const data = await apiRequest<ProductCategoriesResponse>(
|
||||
`${businessPath()}?entityType=${ENTITY_TYPE}`,
|
||||
{ auth: true, signal },
|
||||
)
|
||||
return data.items
|
||||
}
|
||||
|
||||
export async function createProductCategory(payload: CreateCategoryPayload) {
|
||||
return apiRequest<{ message: string; category: ProductCategory }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: { ...payload, entityType: ENTITY_TYPE },
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateProductCategory(
|
||||
categoryId: string,
|
||||
payload: UpdateCategoryPayload,
|
||||
) {
|
||||
return apiRequest<{ message: string; category: ProductCategory }>(
|
||||
businessPath(`/${categoryId}`),
|
||||
{
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function deleteProductCategory(categoryId: string) {
|
||||
return apiRequest<{ message: string; deletedIds: string[] }>(
|
||||
businessPath(`/${categoryId}`),
|
||||
{
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function generateProductCategoriesByAi(prompt: string) {
|
||||
return apiRequest<{ message: string; categories: ProductCategory[] }>(
|
||||
`${businessPath()}/ai-generate`,
|
||||
{
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: { prompt },
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function toCategoryFormPayload(data: CategoryFormData): CreateCategoryPayload {
|
||||
return {
|
||||
parentId: data.parentId || undefined,
|
||||
name: data.nameEn,
|
||||
nameFa: data.nameFa,
|
||||
description: data.description || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapProductCategoryToUi(category: ProductCategory) {
|
||||
return {
|
||||
id: category.id,
|
||||
nameEn: category.name,
|
||||
nameFa: category.nameFa ?? '',
|
||||
description: category.description ?? '',
|
||||
parentId: category.parentId,
|
||||
variationCount: category.variationCount ?? 0,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type { Product } from '../types/product'
|
||||
|
||||
export const PRODUCTS_PER_PAGE = 12
|
||||
|
||||
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
|
||||
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,
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type {
|
||||
CategoryTechnicalForm,
|
||||
ProductTechnicalFormValues,
|
||||
ProductTechnicalValue,
|
||||
} from '../types/technicalForm'
|
||||
|
||||
function businessPath(productId: string) {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/products/${productId}/technical-info`
|
||||
}
|
||||
|
||||
export interface ProductTechnicalInfoResponse {
|
||||
form: CategoryTechnicalForm | null
|
||||
values: ProductTechnicalValue[]
|
||||
message?: string
|
||||
}
|
||||
|
||||
export function valuesToFormState(
|
||||
form: CategoryTechnicalForm | null,
|
||||
values: ProductTechnicalValue[],
|
||||
): ProductTechnicalFormValues {
|
||||
const state: ProductTechnicalFormValues = {}
|
||||
|
||||
if (form) {
|
||||
for (const field of form.fields) {
|
||||
state[field.key] = field.type === 'multi_select' ? [] : ''
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of values) {
|
||||
if (item.value === null || item.value === undefined) continue
|
||||
state[item.fieldKey] = item.value
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
export function formStateToPayload(values: ProductTechnicalFormValues) {
|
||||
return {
|
||||
values: Object.entries(values)
|
||||
.filter(([, value]) => {
|
||||
if (Array.isArray(value)) return value.length > 0
|
||||
return value !== ''
|
||||
})
|
||||
.map(([fieldKey, value]) => ({ fieldKey, value })),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProductTechnicalInfo(productId: string, signal?: AbortSignal) {
|
||||
return apiRequest<ProductTechnicalInfoResponse>(businessPath(productId), {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
}
|
||||
|
||||
export async function saveProductTechnicalInfo(
|
||||
productId: string,
|
||||
values: ProductTechnicalFormValues,
|
||||
) {
|
||||
return apiRequest<ProductTechnicalInfoResponse>(businessPath(productId), {
|
||||
method: 'PUT',
|
||||
auth: true,
|
||||
body: formStateToPayload(values),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type { ProductVariant } from '../types/productVariant'
|
||||
import type { CategoryVariationApi } from './categoryVariationsService'
|
||||
|
||||
export interface ProductVariantApi {
|
||||
id: string
|
||||
selections: {
|
||||
attributeId: string
|
||||
attributeName: string
|
||||
optionId: string
|
||||
value: string
|
||||
}[]
|
||||
label: string
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
export interface CreateProductVariantPayload {
|
||||
variationId: string
|
||||
optionId: string
|
||||
}
|
||||
|
||||
function productVariantsPath(productId: string, suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/products/${productId}/variants${suffix}`
|
||||
}
|
||||
|
||||
export function mapApiVariantToUi(item: ProductVariantApi): ProductVariant {
|
||||
return {
|
||||
id: item.id,
|
||||
selections: item.selections,
|
||||
label: item.label,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapApiVariationsToAttributes(items: CategoryVariationApi[]) {
|
||||
return items.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
options: item.options,
|
||||
}))
|
||||
}
|
||||
|
||||
export type ProductVariantAttribute = ReturnType<
|
||||
typeof mapApiVariationsToAttributes
|
||||
>[number]
|
||||
|
||||
export async function listProductVariants(productId: string, signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ items: ProductVariantApi[] }>(
|
||||
productVariantsPath(productId),
|
||||
{ auth: true, signal },
|
||||
)
|
||||
return data.items.map(mapApiVariantToUi)
|
||||
}
|
||||
|
||||
export async function createProductVariant(
|
||||
productId: string,
|
||||
payload: CreateProductVariantPayload,
|
||||
) {
|
||||
const data = await apiRequest<{ message: string; variant: ProductVariantApi }>(
|
||||
productVariantsPath(productId),
|
||||
{
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
},
|
||||
)
|
||||
return mapApiVariantToUi(data.variant)
|
||||
}
|
||||
|
||||
export async function deleteProductVariant(productId: string, variantId: string) {
|
||||
return apiRequest<{ message: string }>(productVariantsPath(productId, `/${variantId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type { VariationType } from '../types/variation'
|
||||
|
||||
export interface ProductVariationOption {
|
||||
id: string
|
||||
label: string
|
||||
value: string
|
||||
colorHex: string | null
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
export interface ProductVariationSelection {
|
||||
id: string
|
||||
name: string
|
||||
type: VariationType
|
||||
sortOrder: number
|
||||
options: ProductVariationOption[]
|
||||
selectedOptionIds: string[]
|
||||
}
|
||||
|
||||
export interface ProductVariationValuesResponse {
|
||||
variations: ProductVariationSelection[]
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface ProductVariationSelectionPayload {
|
||||
variationId: string
|
||||
optionIds: string[]
|
||||
}
|
||||
|
||||
function businessPath(productId: string) {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/products/${productId}/variations`
|
||||
}
|
||||
|
||||
export function countSelectedOptions(variations: ProductVariationSelection[]) {
|
||||
return variations.reduce((total, variation) => total + variation.selectedOptionIds.length, 0)
|
||||
}
|
||||
|
||||
export function selectionsToPayload(
|
||||
variations: ProductVariationSelection[],
|
||||
): { selections: ProductVariationSelectionPayload[] } {
|
||||
return {
|
||||
selections: variations.map((variation) => ({
|
||||
variationId: variation.id,
|
||||
optionIds: variation.selectedOptionIds,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProductVariationValues(productId: string, signal?: AbortSignal) {
|
||||
return apiRequest<ProductVariationValuesResponse>(businessPath(productId), {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
}
|
||||
|
||||
export async function saveProductVariationValues(
|
||||
productId: string,
|
||||
variations: ProductVariationSelection[],
|
||||
) {
|
||||
return apiRequest<ProductVariationValuesResponse>(businessPath(productId), {
|
||||
method: 'PUT',
|
||||
auth: true,
|
||||
body: selectionsToPayload(variations),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
|
||||
import type { BusinessPrimaryColorId } from '../utils/businessPrimaryColors'
|
||||
|
||||
export interface BrandingSettings {
|
||||
primaryColor: BusinessPrimaryColorId
|
||||
}
|
||||
|
||||
export interface DashboardSettings {
|
||||
comments: { autoApprove: boolean }
|
||||
expertReviews: { autoApprove: boolean }
|
||||
}
|
||||
|
||||
export interface OrderProcessStep {
|
||||
id: string
|
||||
label: string
|
||||
color: string
|
||||
}
|
||||
|
||||
export interface StoreSettings {
|
||||
onlineSellEnabled: boolean
|
||||
orderProcessSteps: OrderProcessStep[]
|
||||
}
|
||||
|
||||
export interface BusinessSettings {
|
||||
branding: BrandingSettings
|
||||
dashboard: DashboardSettings
|
||||
store: StoreSettings
|
||||
}
|
||||
|
||||
export interface SettingsResponse {
|
||||
businessId: string
|
||||
settings: BusinessSettings
|
||||
}
|
||||
|
||||
export const DEFAULT_ORDER_PROCESS_STEPS: OrderProcessStep[] = [
|
||||
{ id: 'processing', label: 'Under processing', color: '#3B82F6' },
|
||||
{ id: 'ready-for-shipping', label: 'Ready for shipping', color: '#F59E0B' },
|
||||
{ id: 'shipped', label: 'Shipped', color: '#8B5CF6' },
|
||||
{ id: 'delivered', label: 'Delivered', color: '#22C55E' },
|
||||
]
|
||||
|
||||
function settingsPath() {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/settings`
|
||||
}
|
||||
|
||||
export async function getSettings(signal?: AbortSignal) {
|
||||
return apiRequest<SettingsResponse>(settingsPath(), { auth: true, signal })
|
||||
}
|
||||
|
||||
export async function updateDashboardSettings(
|
||||
dashboard: Partial<DashboardSettings>,
|
||||
) {
|
||||
return apiRequest<SettingsResponse>(settingsPath(), {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: { dashboard },
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateStoreSettings(store: Partial<StoreSettings>) {
|
||||
return apiRequest<SettingsResponse>(settingsPath(), {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: { store },
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
|
||||
export interface ShoppingCardCustomer {
|
||||
id: string
|
||||
firstName: string | null
|
||||
lastName: string | null
|
||||
cellNumber: string
|
||||
email: string | null
|
||||
}
|
||||
|
||||
export interface ShoppingCardItem {
|
||||
id: string
|
||||
storeItemVariantId: string | null
|
||||
productId: string
|
||||
productTitle: string
|
||||
variantSku: string | null
|
||||
unitPrice: number
|
||||
compareAtPrice: number | null
|
||||
quantity: number
|
||||
lineTotal: number
|
||||
selections: {
|
||||
variationId: string
|
||||
variationName: string
|
||||
optionId: string
|
||||
value: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface ShoppingCard {
|
||||
id: string
|
||||
businessId: string
|
||||
subtotal: number
|
||||
total: number
|
||||
createdBy: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
customer: ShoppingCardCustomer
|
||||
items: ShoppingCardItem[]
|
||||
}
|
||||
|
||||
export interface ShoppingCardsListResponse {
|
||||
items: ShoppingCard[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface ListShoppingCardsParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
customerQuery?: string
|
||||
dateFrom?: string
|
||||
dateTo?: string
|
||||
minTotal?: number
|
||||
maxTotal?: number
|
||||
}
|
||||
|
||||
export interface CreateShoppingCardPayload {
|
||||
customerUserId: string
|
||||
items: { storeItemVariantId: string; quantity: number }[]
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/shopping-cards${suffix}`
|
||||
}
|
||||
|
||||
export async function listShoppingCards(
|
||||
params: ListShoppingCardsParams = {},
|
||||
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.customerQuery) q.set('customerQuery', params.customerQuery)
|
||||
if (params.dateFrom) q.set('dateFrom', params.dateFrom)
|
||||
if (params.dateTo) q.set('dateTo', params.dateTo)
|
||||
if (params.minTotal !== undefined) q.set('minTotal', String(params.minTotal))
|
||||
if (params.maxTotal !== undefined) q.set('maxTotal', String(params.maxTotal))
|
||||
|
||||
const query = q.toString()
|
||||
const path = `${businessPath()}${query ? `?${query}` : ''}`
|
||||
return apiRequest<ShoppingCardsListResponse>(path, { auth: true, signal })
|
||||
}
|
||||
|
||||
export async function getShoppingCard(cardId: string, signal?: AbortSignal) {
|
||||
return apiRequest<{ card: ShoppingCard }>(businessPath(`/${cardId}`), {
|
||||
auth: true,
|
||||
signal,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createShoppingCard(payload: CreateShoppingCardPayload) {
|
||||
return apiRequest<{ message: string; card: ShoppingCard }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function removeShoppingCard(cardId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${cardId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
|
||||
export interface StoreItemSelection {
|
||||
variationId: string
|
||||
variationName: string
|
||||
optionId: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface StoreItem {
|
||||
id: string
|
||||
storeItemId?: string
|
||||
productId: string
|
||||
productTitle: string
|
||||
productNameFa: string
|
||||
productImage: string | null
|
||||
productTotalStock: number
|
||||
selections: StoreItemSelection[]
|
||||
label: string
|
||||
price: number | null
|
||||
discountedPrice: number | null
|
||||
stockQuantity: number | null
|
||||
rewardPoints: number | null
|
||||
isFestival: boolean
|
||||
sortOrder: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface StoreItemsListResponse {
|
||||
items: StoreItem[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface CreateStoreItemPayload {
|
||||
selections: { variationId: string; optionId: string }[]
|
||||
price?: number
|
||||
stockQuantity?: number
|
||||
}
|
||||
|
||||
export interface BatchCreateStoreItemsPayload {
|
||||
productId: string
|
||||
items: CreateStoreItemPayload[]
|
||||
}
|
||||
|
||||
export interface UpdateStoreItemPayload {
|
||||
price?: number
|
||||
stockQuantity?: number
|
||||
discountedPrice?: number | null
|
||||
isFestival?: boolean
|
||||
}
|
||||
|
||||
export interface BatchUpdateStoreItemDiscountsPayload {
|
||||
productId: string
|
||||
items: { id: string; discountedPrice: number | null }[]
|
||||
}
|
||||
|
||||
export interface SyncStoreItemRowPayload {
|
||||
id?: string
|
||||
selections: { variationId: string; optionId: string }[]
|
||||
price: number
|
||||
stockQuantity: number
|
||||
}
|
||||
|
||||
export interface SyncProductStoreItemsPayload {
|
||||
productId: string
|
||||
items: SyncStoreItemRowPayload[]
|
||||
removedIds: string[]
|
||||
}
|
||||
|
||||
export interface BatchUpdateFestivalRewardsPayload {
|
||||
productId: string
|
||||
items: { id: string; rewardPoints: number | null }[]
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/store-items${suffix}`
|
||||
}
|
||||
|
||||
export async function listStoreItems(page = 1, pageSize = 20, signal?: AbortSignal) {
|
||||
return apiRequest<StoreItemsListResponse>(
|
||||
`${businessPath()}?page=${page}&pageSize=${pageSize}`,
|
||||
{ auth: true, signal },
|
||||
)
|
||||
}
|
||||
|
||||
export async function listAllStoreItems(signal?: AbortSignal) {
|
||||
const pageSize = 100
|
||||
let page = 1
|
||||
let items: StoreItem[] = []
|
||||
let total = Infinity
|
||||
|
||||
while (items.length < total) {
|
||||
const data = await listStoreItems(page, pageSize, signal)
|
||||
items = items.concat(data.items)
|
||||
total = data.total
|
||||
page += 1
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
export async function batchCreateStoreItems(payload: BatchCreateStoreItemsPayload) {
|
||||
return apiRequest<{ message: string; items: StoreItem[] }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateStoreItem(itemId: string, payload: UpdateStoreItemPayload) {
|
||||
return apiRequest<{ message: string; item: StoreItem }>(businessPath(`/${itemId}`), {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateStoreItemDiscounts(payload: BatchUpdateStoreItemDiscountsPayload) {
|
||||
return apiRequest<{ message: string; items: StoreItem[] }>(businessPath('/discounts'), {
|
||||
method: 'PUT',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function syncProductStoreItems(payload: SyncProductStoreItemsPayload) {
|
||||
return apiRequest<{ message: string; items: StoreItem[] }>(businessPath('/sync'), {
|
||||
method: 'PUT',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateStoreItemFestivalRewards(payload: BatchUpdateFestivalRewardsPayload) {
|
||||
return apiRequest<{ message: string; items: StoreItem[] }>(businessPath('/festival-rewards'), {
|
||||
method: 'PUT',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteStoreItem(itemId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${itemId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteStoreItemsByProduct(productId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/by-product/${productId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type { StoreSpecial } from '../types/storeSpecial'
|
||||
|
||||
export interface StoreSpecialsListResponse {
|
||||
items: StoreSpecial[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface CreateStoreSpecialPayload {
|
||||
title: string
|
||||
storeItemIds?: string[]
|
||||
sortOrder?: number
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
export interface UpdateStoreSpecialPayload {
|
||||
title?: string
|
||||
storeItemIds?: string[]
|
||||
sortOrder?: number
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/store-specials${suffix}`
|
||||
}
|
||||
|
||||
export async function listStoreSpecials(page = 1, pageSize = 50, signal?: AbortSignal) {
|
||||
return apiRequest<StoreSpecialsListResponse>(
|
||||
`${businessPath()}?page=${page}&pageSize=${pageSize}`,
|
||||
{ auth: true, signal },
|
||||
)
|
||||
}
|
||||
|
||||
export async function createStoreSpecial(payload: CreateStoreSpecialPayload) {
|
||||
return apiRequest<{ message: string; special: StoreSpecial }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateStoreSpecial(specialId: string, payload: UpdateStoreSpecialPayload) {
|
||||
return apiRequest<{ message: string; special: StoreSpecial }>(businessPath(`/${specialId}`), {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteStoreSpecial(specialId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${specialId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
|
||||
export interface ResolvedTenant {
|
||||
id: string
|
||||
name: string
|
||||
nameFa: string | null
|
||||
slug: string
|
||||
domain: string
|
||||
logoUrl?: string | null
|
||||
faviconUrl?: string | null
|
||||
}
|
||||
|
||||
export async function resolveTenantByDomain(host: string) {
|
||||
const encodedHost = encodeURIComponent(host)
|
||||
return apiRequest<ResolvedTenant>(`/tenants/${encodedHost}`)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type {
|
||||
CreateWebsiteBrandGroupPayload,
|
||||
UpdateWebsiteBrandGroupPayload,
|
||||
WebsiteBrandGroup,
|
||||
WebsiteBrandGroupsListResponse,
|
||||
} from '../types/websiteBrandGroup'
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/website/brand-groups${suffix}`
|
||||
}
|
||||
|
||||
export async function listWebsiteBrandGroups(page = 1, pageSize = 50, signal?: AbortSignal) {
|
||||
return apiRequest<WebsiteBrandGroupsListResponse>(
|
||||
`${businessPath()}?page=${page}&pageSize=${pageSize}`,
|
||||
{ auth: true, signal },
|
||||
)
|
||||
}
|
||||
|
||||
export async function createWebsiteBrandGroup(payload: CreateWebsiteBrandGroupPayload) {
|
||||
return apiRequest<{ message: string; group: WebsiteBrandGroup }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateWebsiteBrandGroup(
|
||||
groupId: string,
|
||||
payload: UpdateWebsiteBrandGroupPayload,
|
||||
) {
|
||||
return apiRequest<{ message: string; group: WebsiteBrandGroup }>(businessPath(`/${groupId}`), {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteWebsiteBrandGroup(groupId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${groupId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type {
|
||||
CreateWebsiteCategoryGroupPayload,
|
||||
UpdateWebsiteCategoryGroupPayload,
|
||||
WebsiteCategoryGroup,
|
||||
WebsiteCategoryGroupsListResponse,
|
||||
} from '../types/websiteCategoryGroup'
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/website/category-groups${suffix}`
|
||||
}
|
||||
|
||||
export async function listWebsiteCategoryGroups(page = 1, pageSize = 50, signal?: AbortSignal) {
|
||||
return apiRequest<WebsiteCategoryGroupsListResponse>(
|
||||
`${businessPath()}?page=${page}&pageSize=${pageSize}`,
|
||||
{ auth: true, signal },
|
||||
)
|
||||
}
|
||||
|
||||
export async function createWebsiteCategoryGroup(payload: CreateWebsiteCategoryGroupPayload) {
|
||||
return apiRequest<{ message: string; group: WebsiteCategoryGroup }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateWebsiteCategoryGroup(
|
||||
groupId: string,
|
||||
payload: UpdateWebsiteCategoryGroupPayload,
|
||||
) {
|
||||
return apiRequest<{ message: string; group: WebsiteCategoryGroup }>(
|
||||
businessPath(`/${groupId}`),
|
||||
{
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function deleteWebsiteCategoryGroup(groupId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${groupId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type {
|
||||
CreateWebsiteSliderPayload,
|
||||
UpdateWebsiteSliderPayload,
|
||||
WebsiteSlider,
|
||||
WebsiteSlidersListResponse,
|
||||
} from '../types/websiteSlider'
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/website/sliders${suffix}`
|
||||
}
|
||||
|
||||
export async function listWebsiteSliders(page = 1, pageSize = 50, signal?: AbortSignal) {
|
||||
return apiRequest<WebsiteSlidersListResponse>(
|
||||
`${businessPath()}?page=${page}&pageSize=${pageSize}`,
|
||||
{ auth: true, signal },
|
||||
)
|
||||
}
|
||||
|
||||
export async function createWebsiteSlider(payload: CreateWebsiteSliderPayload) {
|
||||
return apiRequest<{ message: string; slider: WebsiteSlider }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateWebsiteSlider(sliderId: string, payload: UpdateWebsiteSliderPayload) {
|
||||
return apiRequest<{ message: string; slider: WebsiteSlider }>(businessPath(`/${sliderId}`), {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteWebsiteSlider(sliderId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${sliderId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
export function slidesToPayload(
|
||||
slides: Array<{
|
||||
imageMediaId: string
|
||||
title?: string | null
|
||||
linkUrl?: string | null
|
||||
isActive?: boolean
|
||||
}>,
|
||||
) {
|
||||
return slides.map((slide) => ({
|
||||
imageMediaId: slide.imageMediaId,
|
||||
...(slide.title ? { title: slide.title } : {}),
|
||||
...(slide.linkUrl ? { linkUrl: slide.linkUrl } : {}),
|
||||
isActive: slide.isActive ?? true,
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user