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:
Alireza Hassani
2026-07-22 13:48:53 +03:30
co-authored by Cursor
commit f566387c61
509 changed files with 62690 additions and 0 deletions
@@ -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,
})
}
+83
View File
@@ -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()
}
+106
View File
@@ -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,
})
}
+105
View File
@@ -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}`
}