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,49 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
|
||||
export interface UserAddress {
|
||||
id: string
|
||||
label: string | null
|
||||
province: string
|
||||
city: string
|
||||
address: string
|
||||
postalCode: string | null
|
||||
landline: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface UserAddressInput {
|
||||
label?: string
|
||||
province: string
|
||||
city: string
|
||||
address: string
|
||||
postalCode?: string
|
||||
landline?: string
|
||||
}
|
||||
|
||||
export async function listAddresses(signal?: AbortSignal) {
|
||||
return apiRequest<{ items: UserAddress[] }>('/auth/addresses', { auth: true, signal })
|
||||
}
|
||||
|
||||
export async function createAddress(input: UserAddressInput) {
|
||||
return apiRequest<{ address: UserAddress }>('/auth/addresses', {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: input,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateAddress(addressId: string, input: UserAddressInput) {
|
||||
return apiRequest<{ address: UserAddress }>(`/auth/addresses/${addressId}`, {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: input,
|
||||
})
|
||||
}
|
||||
|
||||
export async function removeAddress(addressId: string) {
|
||||
return apiRequest<{ message: string }>(`/auth/addresses/${addressId}`, {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { apiRequest, setTokens, clearTokens } from '../lib/api'
|
||||
import { clearActiveBusiness } from '../lib/businessContext'
|
||||
import type {
|
||||
AuthUser,
|
||||
LoginResponse,
|
||||
MeResponse,
|
||||
OtpSendResponse,
|
||||
OtpVerifyResponse,
|
||||
RegisterResponse,
|
||||
UserProfile,
|
||||
} 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 updateProfile(
|
||||
payload: Partial<UserProfile> & {
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
email?: string
|
||||
},
|
||||
) {
|
||||
return apiRequest<{ message: string; user: AuthUser }>('/auth/profile', {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function changePassword(currentPassword: string, newPassword: string) {
|
||||
return apiRequest<{ message: string }>('/auth/change-password', {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: { currentPassword, newPassword },
|
||||
})
|
||||
}
|
||||
|
||||
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 function logout() {
|
||||
clearTokens()
|
||||
clearActiveBusiness()
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type { Order } from './orderService'
|
||||
|
||||
export interface CartItemSelection {
|
||||
variationId: string
|
||||
variationName: string
|
||||
optionId: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface CartItem {
|
||||
id: string
|
||||
storeItemId: string
|
||||
storeItemVariantId: string
|
||||
productId: string
|
||||
productTitle: string
|
||||
productNameFa: string
|
||||
productImage: string | null
|
||||
sku: string
|
||||
selections: CartItemSelection[]
|
||||
label: string
|
||||
quantity: number
|
||||
unitPrice: number
|
||||
compareAtPrice: number
|
||||
lineTotal: number
|
||||
stockQuantity: number | null
|
||||
}
|
||||
|
||||
export interface Cart {
|
||||
id: string
|
||||
businessId: string
|
||||
items: CartItem[]
|
||||
itemCount: number
|
||||
subtotal: number
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface ShippingAddressInput {
|
||||
province: string
|
||||
city: string
|
||||
address: string
|
||||
postalCode: string
|
||||
landline?: string
|
||||
}
|
||||
|
||||
export type CheckoutPaymentType = 'e_payment_gate' | 'transfer'
|
||||
|
||||
export type OnlineGatewayType = 'mellat_behpardakht' | 'saman_kish'
|
||||
|
||||
export interface CheckoutPaymentInput {
|
||||
type: CheckoutPaymentType
|
||||
gatewayType?: string
|
||||
transferAccount?: string
|
||||
transferRefNumber?: string
|
||||
}
|
||||
|
||||
export interface CheckoutInput {
|
||||
addressId?: string
|
||||
shippingAddress?: ShippingAddressInput
|
||||
customerNotes?: string
|
||||
payment: CheckoutPaymentInput
|
||||
}
|
||||
|
||||
function cartPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/cart${suffix}`
|
||||
}
|
||||
|
||||
export async function getCart(signal?: AbortSignal) {
|
||||
return apiRequest<{ cart: Cart }>(cartPath(), { auth: true, signal })
|
||||
}
|
||||
|
||||
export async function addCartItem(storeItemVariantId: string, quantity = 1) {
|
||||
return apiRequest<{ message: string; cart: Cart }>(cartPath('/items'), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: { storeItemVariantId, quantity },
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateCartItem(itemId: string, quantity: number) {
|
||||
return apiRequest<{ message: string; cart: Cart }>(cartPath(`/items/${itemId}`), {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: { quantity },
|
||||
})
|
||||
}
|
||||
|
||||
export async function removeCartItem(itemId: string) {
|
||||
return apiRequest<{ message: string; cart: Cart }>(cartPath(`/items/${itemId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
export async function checkoutCart(input: CheckoutInput) {
|
||||
return apiRequest<{ message: string; order: Order }>(cartPath('/checkout'), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: input,
|
||||
})
|
||||
}
|
||||
@@ -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,53 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
|
||||
export interface FavoriteListing {
|
||||
favoriteId: string
|
||||
productId: string
|
||||
createdAt: string
|
||||
productTitle: string
|
||||
productNameFa: string
|
||||
productImage: string | null
|
||||
productTotalStock: number
|
||||
variantCount: number
|
||||
displayPrice: number | null
|
||||
displayDiscountedPrice: number | null
|
||||
showFestival: boolean
|
||||
}
|
||||
|
||||
export interface FavoritesListResponse {
|
||||
items: FavoriteListing[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface ListFavoritesParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/favorites${suffix}`
|
||||
}
|
||||
|
||||
export async function listFavorites(params: ListFavoritesParams = {}, signal?: AbortSignal) {
|
||||
const q = new URLSearchParams()
|
||||
if (params.page !== undefined) q.set('page', String(params.page))
|
||||
if (params.pageSize !== undefined) q.set('pageSize', String(params.pageSize))
|
||||
|
||||
const query = q.toString()
|
||||
const path = `${businessPath()}${query ? `?${query}` : ''}`
|
||||
return apiRequest<FavoritesListResponse>(path, { auth: true, signal })
|
||||
}
|
||||
|
||||
export async function removeFavorite(productId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${productId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
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 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[]
|
||||
}
|
||||
|
||||
export interface OrdersListResponse {
|
||||
items: Order[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface ListOrdersParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: OrderStatus
|
||||
orderNumber?: string
|
||||
dateFrom?: string
|
||||
dateTo?: string
|
||||
}
|
||||
|
||||
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.dateFrom) q.set('dateFrom', params.dateFrom)
|
||||
if (params.dateTo) q.set('dateTo', params.dateTo)
|
||||
|
||||
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 })
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
clearGuestCart,
|
||||
loadGuestCart,
|
||||
type GuestCartItem,
|
||||
} from '../lib/guestCart'
|
||||
import { addCartItem, getCart, type Cart } from './cartService'
|
||||
|
||||
/**
|
||||
* Push guest-cart lines into the authenticated server cart.
|
||||
* Guest item `id` is the storeItemVariantId from the storefront.
|
||||
*/
|
||||
export async function syncGuestCartToServer(signal?: AbortSignal): Promise<Cart> {
|
||||
const guestItems = loadGuestCart()
|
||||
if (guestItems.length === 0) {
|
||||
const data = await getCart(signal)
|
||||
return data.cart
|
||||
}
|
||||
|
||||
let cart: Cart | null = null
|
||||
|
||||
for (const item of guestItems) {
|
||||
if (signal?.aborted) break
|
||||
const variantId = resolveVariantId(item)
|
||||
if (!variantId) continue
|
||||
const quantity = Math.max(1, Math.floor(item.quantity) || 1)
|
||||
const result = await addCartItem(variantId, quantity)
|
||||
cart = result.cart
|
||||
}
|
||||
|
||||
if (!cart) {
|
||||
const data = await getCart(signal)
|
||||
cart = data.cart
|
||||
}
|
||||
|
||||
if (cart.items.length > 0) {
|
||||
clearGuestCart()
|
||||
}
|
||||
|
||||
return cart
|
||||
}
|
||||
|
||||
function resolveVariantId(item: GuestCartItem): string | null {
|
||||
const id = item.id?.trim()
|
||||
if (!id) return null
|
||||
// Storefront guest cart uses variant id as item id
|
||||
return id
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import type { BusinessPrimaryColorId } from '../utils/businessPrimaryColors'
|
||||
|
||||
export interface ResolvedTenant {
|
||||
id: string
|
||||
name: string
|
||||
nameFa: string | null
|
||||
slug: string
|
||||
domain: string
|
||||
primaryColor: BusinessPrimaryColorId
|
||||
logoUrl?: string | null
|
||||
faviconUrl?: string | null
|
||||
}
|
||||
|
||||
export async function resolveTenantByDomain(host: string, signal?: AbortSignal) {
|
||||
const encodedHost = encodeURIComponent(host)
|
||||
return apiRequest<ResolvedTenant>(`/tenants/${encodedHost}`, { signal })
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getTenantDomain } from '../lib/config'
|
||||
|
||||
export interface WebsiteBusinessInfo {
|
||||
id: string
|
||||
name: string
|
||||
nameFa: string
|
||||
logoUrl: string | null
|
||||
faviconUrl: string | null
|
||||
}
|
||||
|
||||
export async function getWebsiteBusinessInfo(host: string, signal?: AbortSignal) {
|
||||
const encodedHost = encodeURIComponent(host)
|
||||
return apiRequest<WebsiteBusinessInfo>(`/tenants/${encodedHost}/website/business-info`, {
|
||||
signal,
|
||||
})
|
||||
}
|
||||
|
||||
/** Public storefront URL for the tenant (e.g. https://sanihome.ir). */
|
||||
export function getWebsiteUrl(domain = getTenantDomain()) {
|
||||
return `${window.location.protocol}//${domain}`
|
||||
}
|
||||
Reference in New Issue
Block a user