Files
nademitex_admin/src/services/api.ts
T
safavimohadeseh78andCursor f68fd820e7 Align admin orders with the shop pre-order flow.
Admins create product-based pre-orders, price real line items, reach final approval after cash or checks, and only ship eligible orders.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 21:41:09 +03:30

633 lines
20 KiB
TypeScript

import { apiRequest } from '@/services/http'
import type {
AdminNotification,
AdminUser,
BankAccount,
CashPayment,
Catalog,
Check,
CheckDeadline,
Company,
Customer,
InternalMessage,
Order,
OrderItem,
OrderStatus,
OrderStatusHistoryItem,
Product,
RawOrderStatus,
SettlementMethod,
Shipment,
ShipmentStatus,
UserRole,
} from '@/types'
/** Normalize admin list order status badges */
export function mapAdminOrder<
T extends {
status: string
adminStatus?: string
allowedTransitions?: string[]
settlementMethod?: string | null
settlementType?: string | null
meterage?: number
totalMeterage?: number
},
>(
order: T,
): T & {
status: OrderStatus
rawStatus: RawOrderStatus
allowedTransitions: RawOrderStatus[]
settlementMethod: SettlementMethod | null
meterage: number
} {
return {
...order,
status: (order.adminStatus ?? order.status) as OrderStatus,
rawStatus: order.status as RawOrderStatus,
allowedTransitions: (order.allowedTransitions ?? []) as RawOrderStatus[],
settlementMethod: (order.settlementMethod ??
order.settlementType ??
null) as SettlementMethod | null,
meterage: order.meterage ?? order.totalMeterage ?? 0,
}
}
export function slugifyCompanyName(name: string): string {
const slug = name
.trim()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
return slug || `c-${Date.now()}`
}
/* ── Companies ─────────────────────────────────────────────── */
export type CreateCompanyPayload = {
name: string
slug: string
description?: string
logoUrl?: string | null
displayOrder?: number
isActive?: boolean
}
export type UpdateCompanyPayload = Partial<Omit<CreateCompanyPayload, 'slug'>> & {
slug?: string
}
export const companiesApi = {
list: (activeOnly?: boolean) =>
apiRequest<Company[]>('/companies', { query: { activeOnly: activeOnly ? 'true' : undefined } }),
get: (id: string) => apiRequest<Company>(`/companies/${id}`),
create: (body: CreateCompanyPayload) => apiRequest<Company>('/companies', { body }),
update: (id: string, body: UpdateCompanyPayload) =>
apiRequest<Company>(`/companies/${id}`, { method: 'PATCH', body }),
setActive: (id: string, isActive: boolean) =>
apiRequest<Company>(`/companies/${id}/active`, { method: 'PATCH', body: { isActive } }),
}
/* ── Bank accounts ─────────────────────────────────────────── */
export type CreateBankAccountPayload = {
bankName: string
accountHolder: string
accountNumber: string
cardNumber: string
iban: string
displayOrder?: number
isActive?: boolean
notes?: string
}
export type UpdateBankAccountPayload = Partial<CreateBankAccountPayload>
export const bankAccountsApi = {
listByCompany: (companyId: string) =>
apiRequest<BankAccount[]>(`/companies/${companyId}/bank-accounts`),
create: (companyId: string, body: CreateBankAccountPayload) =>
apiRequest<BankAccount>(`/companies/${companyId}/bank-accounts`, { body }),
update: (id: string, body: UpdateBankAccountPayload) =>
apiRequest<BankAccount>(`/bank-accounts/${id}`, { method: 'PATCH', body }),
setActive: (id: string, isActive: boolean) =>
apiRequest<BankAccount>(`/bank-accounts/${id}/active`, { method: 'PATCH', body: { isActive } }),
}
/* ── Catalogs ──────────────────────────────────────────────── */
export type CreateCatalogPayload = {
companyId: string
name: string
code: string
description?: string
displayOrder?: number
isActive?: boolean
}
export type UpdateCatalogPayload = Partial<Omit<CreateCatalogPayload, 'companyId'>>
export const catalogsApi = {
list: (params?: { companyId?: string; activeOnly?: boolean }) =>
apiRequest<Catalog[]>('/catalogs', {
query: {
companyId: params?.companyId,
activeOnly: params?.activeOnly ? 'true' : undefined,
},
}),
create: (body: CreateCatalogPayload) => apiRequest<Catalog>('/catalogs', { body }),
update: (id: string, body: UpdateCatalogPayload) =>
apiRequest<Catalog>(`/catalogs/${id}`, { method: 'PATCH', body }),
setActive: (id: string, isActive: boolean) =>
apiRequest<Catalog>(`/catalogs/${id}/active`, { method: 'PATCH', body: { isActive } }),
}
/* ── Products ──────────────────────────────────────────────── */
export type CreateProductPayload = {
companyId: string
catalogId: string
name: string
code?: string
productCode?: string
color: string
minMeterage?: number
orderStep?: number
orderMultiplier?: number
price?: number | null
basePrice?: number | null
internalStock?: number | null
stock?: number | null
isActive?: boolean
description?: string
usageCategory?: string
season?: string
composition?: string | null
weaveType?: string | null
subtitle?: string | null
rollMeters?: number
widthCm?: number | null
weightGrams?: number | null
isNew?: boolean
colorsJson?: string[]
imageUrlsJson?: string[]
tagsJson?: string[]
}
export type UpdateProductPayload = Partial<Omit<CreateProductPayload, 'companyId'>>
export const productsApi = {
list: (params?: { catalogId?: string; companyId?: string; activeOnly?: boolean }) =>
apiRequest<Product[]>('/products', {
query: {
catalogId: params?.catalogId,
companyId: params?.companyId,
activeOnly: params?.activeOnly ? 'true' : undefined,
},
}),
create: (body: CreateProductPayload) => apiRequest<Product>('/products', { body }),
update: (id: string, body: UpdateProductPayload) =>
apiRequest<Product>(`/products/${id}`, { method: 'PATCH', body }),
setActive: (id: string, isActive: boolean) =>
apiRequest<Product>(`/products/${id}/active`, { method: 'PATCH', body: { isActive } }),
}
/* ── Customers ─────────────────────────────────────────────── */
export type CreateCustomerPayload = {
name?: string
firstName?: string
lastName?: string
phone: string
nationalId?: string
customerType?: Customer['customerType']
companyId?: string | null
isActive?: boolean
levelDefId?: string | null
assignedAgentId?: string | null
appRole?: 'customer' | 'agent'
agentCode?: string | null
brandIds?: string[]
}
export type UpdateCustomerPayload = Partial<CreateCustomerPayload>
export const customersApi = {
list: (q?: string, appRole?: 'customer' | 'agent') =>
apiRequest<Customer[]>('/admin/customers', { query: { q, appRole } }),
create: (body: CreateCustomerPayload) => apiRequest<Customer>('/admin/customers', { body }),
update: (id: string, body: UpdateCustomerPayload) =>
apiRequest<Customer>(`/admin/customers/${id}`, { method: 'PATCH', body }),
setActive: (id: string, isActive: boolean) =>
apiRequest<Customer>(`/admin/customers/${id}/active`, { method: 'PATCH', body: { isActive } }),
}
/* ── Orders ────────────────────────────────────────────────── */
export type AdminCreateOrderPayload = {
companyId?: string
customerId?: string
customerName?: string
customerPhone?: string
userId?: string
settlementMethod?: SettlementMethod
settlementType?: SettlementMethod
meterage?: number
notes?: string
items?: Array<{ productId: string; meterage: number; notes?: string }>
}
export type OrderItemPricingPayload = {
id?: string
productId?: string
requestedMeterage?: number
confirmedMeterage?: number
unitPrice?: number
supplyPrice?: number
}
export type UpdateOrderPricingPayload = {
items: OrderItemPricingPayload[]
shippingCost?: number
note?: string
deletedItemIds?: string[]
}
export type ApiOrder = Order & {
adminStatus?: string
settlementType?: SettlementMethod | null
totalMeterage?: number
shippingCost?: number | null
finalAmount?: number | null
checkRassDays?: number | null
checkRassDate?: string
items?: OrderItem[]
checks?: Check[]
statusHistory?: OrderStatusHistoryItem[]
/** وضعیت‌های مجاز بعدی از دید ماشین وضعیت سرور */
allowedTransitions?: string[]
cashReceipt?: { id: string } | null
}
export type UpdateOrderStatusPayload = {
status: string
note?: string
reason?: string
/** گذر خارج از ماشین وضعیت؛ سرور reason را اجباری می‌کند */
force?: boolean
}
export const ordersApi = {
list: (params?: { status?: string; q?: string }) =>
apiRequest<ApiOrder[]>('/admin/orders', { query: { status: params?.status, q: params?.q } }),
get: (id: string) => apiRequest<ApiOrder>(`/admin/orders/${id}`),
create: (body: AdminCreateOrderPayload) => apiRequest<ApiOrder>('/admin/orders', { body }),
updatePricing: (id: string, body: UpdateOrderPricingPayload) =>
apiRequest<ApiOrder>(`/admin/orders/${id}/pricing`, { method: 'PATCH', body }),
updateStatus: (id: string, body: UpdateOrderStatusPayload) =>
apiRequest<ApiOrder>(`/admin/orders/${id}/status`, { method: 'PATCH', body }),
}
/* ── Checks ────────────────────────────────────────────────── */
export const checksApi = {
list: (status?: Check['status']) =>
apiRequest<Check[]>('/admin/checks', { query: { status } }),
listByOrder: (orderId: string) =>
apiRequest<Check[]>(`/admin/orders/${orderId}/checks`),
updateStatus: (id: string, body: { status: Check['status']; note?: string }) =>
apiRequest<Check>(`/admin/checks/${id}/status`, { method: 'PATCH', body }),
}
/* ── Cash payments ─────────────────────────────────────────── */
export const cashPaymentsApi = {
list: (status?: CashPayment['status']) =>
apiRequest<CashPayment[]>('/admin/cash-payments', { query: { status } }),
updateStatus: (id: string, body: { status: CashPayment['status']; note?: string }) =>
apiRequest<CashPayment>(`/admin/cash-payments/${id}/status`, { method: 'PATCH', body }),
}
/* ── Shipments ─────────────────────────────────────────────── */
export type CreateShipmentPayload = {
orderId: string
carrier: string
trackingNumber?: string
meterage?: number
status?: ShipmentStatus
}
export const shipmentsApi = {
list: (status?: ShipmentStatus) =>
apiRequest<Shipment[]>('/admin/shipments', { query: { status } }),
create: (body: CreateShipmentPayload) => apiRequest<Shipment>('/admin/shipments', { body }),
update: (id: string, body: Partial<CreateShipmentPayload>) =>
apiRequest<Shipment>(`/admin/shipments/${id}`, { method: 'PATCH', body }),
advanceStatus: (id: string, body?: { status?: ShipmentStatus; trackingNumber?: string }) =>
apiRequest<Shipment>(`/admin/shipments/${id}/status`, { method: 'PATCH', body: body ?? {} }),
}
/* ── Admin users ───────────────────────────────────────────── */
export type CreateAdminUserPayload = {
fullName: string
phone: string
email?: string
password?: string
role?: UserRole
isActive?: boolean
}
export type UpdateAdminUserPayload = Partial<CreateAdminUserPayload>
export const adminUsersApi = {
list: () => apiRequest<AdminUser[]>('/admin/users'),
create: (body: CreateAdminUserPayload) => apiRequest<AdminUser>('/admin/users', { body }),
update: (id: string, body: UpdateAdminUserPayload) =>
apiRequest<AdminUser>(`/admin/users/${id}`, { method: 'PATCH', body }),
setActive: (id: string, isActive: boolean) =>
apiRequest<AdminUser>(`/admin/users/${id}/active`, { method: 'PATCH', body: { isActive } }),
}
/* ── Dashboard & reports ───────────────────────────────────── */
export type DashboardStats = {
newOrders: number
pendingReview: number
pendingCheck: number
finalApproval: number
todaySales: number
todaySalesGrowth: number
monthSales: number
}
export type DashboardResponse = {
stats: DashboardStats
recentOrders: ApiOrder[]
salesChartData: number[]
chartAxisLabels: string[]
checkDeadlines: CheckDeadline[]
notifications?: AdminNotification[]
messages?: InternalMessage[]
}
export type ReportsResponse = {
reportSalesByCompany: {
companyName: string
orderCount: number
totalAmount: number
meterage: number
}[]
reportOrdersByStatus: { status: string; count: number }[]
reportSalesByMonth?: {
month: string
orderCount: number
totalAmount: number
meterage: number
}[]
reportSalesByProduct?: {
productId: string
productName: string
productCode: string
orderCount: number
meterage: number
totalAmount: number
}[]
reportSalesByCustomer?: {
customerName: string
orderCount: number
totalAmount: number
meterage: number
}[]
reportCustomers?: {
active: number
inactive: number
newLast30Days: number
noPurchase: number
noPurchase90Days: number
withAgent: number
}
}
export const dashboardApi = {
get: () => apiRequest<DashboardResponse>('/admin/dashboard'),
reports: () => apiRequest<ReportsResponse>('/admin/reports'),
}
/* ── System settings ──────────────────────────────────────── */
export type SystemSettings = {
id: string
maxMeteragePerInvoice: number
cashInvoiceValidityHours: number
defaultSettlementCheck: boolean
defaultSettlementCash: boolean
minOrderRollsPerProduct?: number
showCatalogPublicPrice?: boolean
allowAgentPricing?: boolean
agentMinUnitPrice?: number | null
smsOrderRegistered: boolean
smsPricingReady: boolean
smsPaymentReminder: boolean
smsCheckReview: boolean
smsShippingUpdate: boolean
supportPhone: string | null
supportEmail: string | null
categoryImagesJson?: Record<string, string>
updatedAt: string
}
export type UpdateSettingsPayload = Partial<Omit<SystemSettings, 'id' | 'updatedAt'>>
export const settingsApi = {
get: () => apiRequest<SystemSettings>('/admin/settings'),
update: (body: UpdateSettingsPayload) =>
apiRequest<SystemSettings>('/admin/settings', { method: 'PATCH', body }),
}
/* ── Admin notifications ──────────────────────────────────── */
export const notificationsApi = {
markRead: (id: string) =>
apiRequest<{ ok: boolean }>(`/admin/notifications/${id}/read`, { method: 'PATCH' }),
markAllRead: () =>
apiRequest<{ ok: boolean }>('/admin/notifications/read-all', { method: 'PATCH' }),
broadcast: (body: { title: string; body: string; customerIds?: string[]; tone?: string }) =>
apiRequest<{ sent: number }>('/admin/notifications/broadcast', { method: 'POST', body }),
}
/* ── Catalog requests (physical) ──────────────────────────── */
export type CatalogRequestRow = {
id: string
customerId: string
customerName: string
customerPhone: string
status: string
note?: string
createdAt: string
items: {
id: string
productId: string
productName: string
productCode: string
quantity: number
holderType: string
holderCustomerId?: string | null
movements?: {
id: string
fromHolder: string
toHolder: string
note?: string
movedAt: string
}[]
}[]
}
export const catalogRequestsApi = {
list: (status?: string) =>
apiRequest<CatalogRequestRow[]>('/admin/catalog-requests', {
query: { status },
}),
updateStatus: (id: string, status: string) =>
apiRequest<CatalogRequestRow>(`/admin/catalog-requests/${id}/status`, {
method: 'PATCH',
body: { status },
}),
updateHolder: (
itemId: string,
body: { holderType: string; holderCustomerId?: string | null; note?: string },
) =>
apiRequest(`/admin/catalog-request-items/${itemId}/holder`, {
method: 'PATCH',
body,
}),
}
/* ── Brands ───────────────────────────────────────────────── */
export type Brand = {
id: string
name: string
slug?: string
description?: string
isActive: boolean
displayOrder: number
customersCount?: number
}
export type AppBanner = {
id: string
slot: string
title: string
subtitle: string
ctaLabel: string
ctaLink: string
imageUrl: string
displayOrder: number
isActive: boolean
}
export type BannerPayload = {
slot: string
title: string
subtitle?: string | null
ctaLabel?: string | null
ctaLink?: string | null
imageUrl?: string | null
displayOrder?: number
isActive?: boolean
}
export const bannersApi = {
list: (slot?: string) =>
apiRequest<AppBanner[]>('/admin/banners', { query: { slot } }),
create: (body: BannerPayload) =>
apiRequest<AppBanner>('/admin/banners', { body }),
update: (id: string, body: Partial<BannerPayload>) =>
apiRequest<AppBanner>(`/admin/banners/${id}`, { method: 'PATCH', body }),
remove: (id: string) =>
apiRequest<{ ok: boolean }>(`/admin/banners/${id}`, { method: 'DELETE' }),
}
export const brandsApi = {
list: () => apiRequest<Brand[]>('/admin/brands'),
create: (body: Partial<Brand> & { name: string }) =>
apiRequest<Brand>('/admin/brands', { body }),
update: (id: string, body: Partial<Brand>) =>
apiRequest<Brand>(`/admin/brands/${id}`, { method: 'PATCH', body }),
}
/* ── Levels ───────────────────────────────────────────────── */
export type CustomerLevel = {
id: string
code: string
label: string
sortOrder: number
isActive: boolean
customersCount: number
benefits: { id?: string; title: string; description?: string }[]
}
export const levelsApi = {
list: () => apiRequest<CustomerLevel[]>('/admin/levels'),
create: (body: {
code: string
label: string
sortOrder?: number
benefits?: { title: string; description?: string }[]
}) => apiRequest<CustomerLevel>('/admin/levels', { body }),
update: (id: string, body: Partial<CustomerLevel> & { benefits?: { title: string; description?: string }[] }) =>
apiRequest<CustomerLevel>(`/admin/levels/${id}`, { method: 'PATCH', body }),
}
/* ── Suggestions ──────────────────────────────────────────── */
export type ProductSuggestion = {
id: string
customerId: string
customerName: string
productId: string
productName: string
productCode: string
note?: string
isActive: boolean
createdAt: string
}
export const suggestionsApi = {
list: (customerId?: string) =>
apiRequest<ProductSuggestion[]>('/admin/suggestions', { query: { customerId } }),
create: (body: { customerId: string; productId: string; note?: string }) =>
apiRequest<ProductSuggestion>('/admin/suggestions', { body }),
setActive: (id: string, isActive: boolean) =>
apiRequest<ProductSuggestion>(`/admin/suggestions/${id}/active`, {
method: 'PATCH',
body: { isActive },
}),
}
/* ── Stock alerts ─────────────────────────────────────────── */
export type StockAlert = {
id: string
customerName: string
customerPhone: string
productName: string
productCode: string
stock: number | null
active: boolean
createdAt: string
}
export const stockAlertsApi = {
list: () => apiRequest<StockAlert[]>('/admin/stock-alerts'),
notify: (id: string) =>
apiRequest<StockAlert>(`/admin/stock-alerts/${id}/notify`, { method: 'PATCH' }),
}