Initial commit of Balout Pastry admin dashboards.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+119
@@ -0,0 +1,119 @@
|
||||
import { getAccessToken, getRefreshToken, setSession, clearSession } from './auth'
|
||||
|
||||
const API_BASE_URL =
|
||||
import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3100/api/v1'
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
|
||||
type RequestOptions = {
|
||||
method?: string
|
||||
body?: unknown
|
||||
auth?: boolean
|
||||
/** Skip refresh retry (used by refresh itself). */
|
||||
skipRefresh?: boolean
|
||||
}
|
||||
|
||||
let refreshInFlight: Promise<boolean> | null = null
|
||||
|
||||
function parseErrorMessage(payload: unknown, fallback: string) {
|
||||
if (!payload || typeof payload !== 'object') return fallback
|
||||
const message = (payload as { message?: unknown }).message
|
||||
if (typeof message === 'string' && message.trim()) return message
|
||||
if (Array.isArray(message)) {
|
||||
const parts = message.filter((item) => typeof item === 'string')
|
||||
if (parts.length > 0) return parts.join('، ')
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
async function tryRefreshSession(): Promise<boolean> {
|
||||
if (refreshInFlight) return refreshInFlight
|
||||
|
||||
refreshInFlight = (async () => {
|
||||
const refreshToken = getRefreshToken()
|
||||
if (!refreshToken) {
|
||||
clearSession()
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
})
|
||||
|
||||
const payload = await response.json().catch(() => null)
|
||||
if (!response.ok) {
|
||||
clearSession()
|
||||
return false
|
||||
}
|
||||
|
||||
setSession({
|
||||
accessToken: payload.accessToken,
|
||||
refreshToken: payload.refreshToken,
|
||||
user: payload.user,
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
clearSession()
|
||||
return false
|
||||
} finally {
|
||||
refreshInFlight = null
|
||||
}
|
||||
})()
|
||||
|
||||
return refreshInFlight
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(
|
||||
path: string,
|
||||
options: RequestOptions = {},
|
||||
): Promise<T> {
|
||||
const { method = 'GET', body, auth = true, skipRefresh = false } = options
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
if (body !== undefined) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
if (auth) {
|
||||
const token = getAccessToken()
|
||||
if (token) headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
})
|
||||
|
||||
if (response.status === 401 && auth && !skipRefresh) {
|
||||
const refreshed = await tryRefreshSession()
|
||||
if (refreshed) {
|
||||
return apiRequest<T>(path, { ...options, skipRefresh: true })
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return undefined as T
|
||||
}
|
||||
|
||||
const payload = await response.json().catch(() => null)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ApiError(
|
||||
response.status,
|
||||
parseErrorMessage(payload, 'خطایی رخ داد'),
|
||||
)
|
||||
}
|
||||
|
||||
return payload as T
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { UserRole } from '../data/users'
|
||||
|
||||
const AUTH_KEY = 'balout.admin.auth'
|
||||
|
||||
export type AuthUser = {
|
||||
id: string
|
||||
title: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
cellNumber: string
|
||||
role: UserRole
|
||||
name: string
|
||||
}
|
||||
|
||||
export type AuthSession = {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
user: AuthUser
|
||||
}
|
||||
|
||||
function isSession(value: unknown): value is AuthSession {
|
||||
if (!value || typeof value !== 'object') return false
|
||||
const session = value as Partial<AuthSession>
|
||||
return (
|
||||
typeof session.accessToken === 'string' &&
|
||||
typeof session.refreshToken === 'string' &&
|
||||
!!session.user &&
|
||||
typeof session.user === 'object' &&
|
||||
typeof session.user.name === 'string' &&
|
||||
typeof session.user.id === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
export function getSession(): AuthSession | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(AUTH_KEY)
|
||||
if (!raw) return null
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
return isSession(parsed) ? parsed : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function setSession(session: AuthSession) {
|
||||
localStorage.setItem(AUTH_KEY, JSON.stringify(session))
|
||||
}
|
||||
|
||||
export function clearSession() {
|
||||
localStorage.removeItem(AUTH_KEY)
|
||||
}
|
||||
|
||||
export function isAuthenticated(): boolean {
|
||||
return getSession() !== null
|
||||
}
|
||||
|
||||
export function getAccessToken(): string | null {
|
||||
return getSession()?.accessToken ?? null
|
||||
}
|
||||
|
||||
export function getRefreshToken(): string | null {
|
||||
return getSession()?.refreshToken ?? null
|
||||
}
|
||||
|
||||
export function getAuthUser(): AuthUser | null {
|
||||
return getSession()?.user ?? null
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { apiRequest } from './api'
|
||||
import { clearSession, getRefreshToken, setSession, type AuthUser } from './auth'
|
||||
|
||||
export type AuthTokensResponse = {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
user: AuthUser
|
||||
}
|
||||
|
||||
export async function login(cellNumber: string, password: string) {
|
||||
const data = await apiRequest<AuthTokensResponse>('/auth/login', {
|
||||
method: 'POST',
|
||||
auth: false,
|
||||
body: { cellNumber, password },
|
||||
})
|
||||
setSession({
|
||||
accessToken: data.accessToken,
|
||||
refreshToken: data.refreshToken,
|
||||
user: data.user,
|
||||
})
|
||||
return data.user
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
const refreshToken = getRefreshToken()
|
||||
try {
|
||||
if (refreshToken) {
|
||||
await apiRequest('/auth/logout', {
|
||||
method: 'POST',
|
||||
auth: false,
|
||||
skipRefresh: true,
|
||||
body: { refreshToken },
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// Always clear local session even if logout request fails.
|
||||
} finally {
|
||||
clearSession()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
export type BranchRecord = {
|
||||
id: string
|
||||
name: string
|
||||
district: string
|
||||
address: string
|
||||
landline: string
|
||||
cellNumber: string
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'balout-branches'
|
||||
|
||||
const seedBranches: BranchRecord[] = [
|
||||
{
|
||||
id: 'branch-1',
|
||||
name: 'شعبه مرکزی',
|
||||
district: 'حرم',
|
||||
address: 'قم، خیابان ارم، نبش کوچه ۳',
|
||||
landline: '02531230001',
|
||||
cellNumber: '09120000001',
|
||||
},
|
||||
{
|
||||
id: 'branch-2',
|
||||
name: 'شعبه صفاییه',
|
||||
district: 'صفاییه',
|
||||
address: 'قم، صفاییه، بلوار شهید بهشتی',
|
||||
landline: '02531230002',
|
||||
cellNumber: '09120000002',
|
||||
},
|
||||
{
|
||||
id: 'branch-3',
|
||||
name: 'شعبه نیروی هوایی',
|
||||
district: 'نیروگاه',
|
||||
address: 'قم، شهرک نیروی هوایی، فاز ۲',
|
||||
landline: '02531230003',
|
||||
cellNumber: '09120000003',
|
||||
},
|
||||
]
|
||||
|
||||
function readBranches(): BranchRecord[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(seedBranches))
|
||||
return seedBranches
|
||||
}
|
||||
const parsed = JSON.parse(raw) as BranchRecord[]
|
||||
return parsed.length > 0 ? parsed : seedBranches
|
||||
} catch {
|
||||
return seedBranches
|
||||
}
|
||||
}
|
||||
|
||||
export function getBranches(): BranchRecord[] {
|
||||
return readBranches().filter((branch) => branch.name.trim())
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
createFlavorBlock,
|
||||
createFlavorEntry,
|
||||
type CategoryFlavorOptions,
|
||||
} from '../data/flavors'
|
||||
|
||||
const STORAGE_KEY = 'balout-category-flavor-options'
|
||||
|
||||
type OptionsMap = Record<string, CategoryFlavorOptions>
|
||||
|
||||
function seedOptions(): OptionsMap {
|
||||
return {
|
||||
شیرینی: [
|
||||
{
|
||||
...createFlavorBlock('chocolate'),
|
||||
entries: [
|
||||
createFlavorEntry('۱۰۰ گرم', 25000),
|
||||
createFlavorEntry('۵۰۰ گرم', 50000),
|
||||
],
|
||||
},
|
||||
{
|
||||
...createFlavorBlock('pistachio'),
|
||||
entries: [
|
||||
createFlavorEntry('۱۵۰ گرم', 45000),
|
||||
createFlavorEntry('۱ کیلو', 280000),
|
||||
],
|
||||
},
|
||||
],
|
||||
کیک: [
|
||||
{
|
||||
...createFlavorBlock('vanilla'),
|
||||
entries: [
|
||||
createFlavorEntry('نیم کیلو', 220000),
|
||||
createFlavorEntry('۱ کیلو', 420000),
|
||||
],
|
||||
},
|
||||
{
|
||||
...createFlavorBlock('chocolate'),
|
||||
entries: [createFlavorEntry('۱ کیلو', 450000)],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function readStore(): OptionsMap {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) {
|
||||
const seeded = seedOptions()
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(seeded))
|
||||
return seeded
|
||||
}
|
||||
return JSON.parse(raw) as OptionsMap
|
||||
} catch {
|
||||
return seedOptions()
|
||||
}
|
||||
}
|
||||
|
||||
function writeStore(map: OptionsMap) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(map))
|
||||
}
|
||||
|
||||
export function getAllCategoryOptions(): OptionsMap {
|
||||
return readStore()
|
||||
}
|
||||
|
||||
export function getCategoryOptionsByName(
|
||||
categoryNameFa: string,
|
||||
): CategoryFlavorOptions {
|
||||
return readStore()[categoryNameFa] ?? []
|
||||
}
|
||||
|
||||
export function setCategoryOptionsByName(
|
||||
categoryNameFa: string,
|
||||
options: CategoryFlavorOptions,
|
||||
) {
|
||||
const map = readStore()
|
||||
map[categoryNameFa] = options
|
||||
writeStore(map)
|
||||
}
|
||||
|
||||
export function removeCategoryOptionsByName(categoryNameFa: string) {
|
||||
const map = readStore()
|
||||
delete map[categoryNameFa]
|
||||
writeStore(map)
|
||||
}
|
||||
|
||||
/** Available options that have at least one amount/price value. */
|
||||
export function getSelectableCategoryOptions(categoryNameFa: string) {
|
||||
return getCategoryOptionsByName(categoryNameFa).filter(
|
||||
(block) => block.entries.length > 0,
|
||||
)
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
CakeSlice,
|
||||
FolderTree,
|
||||
Package,
|
||||
PlusCircle,
|
||||
Settings,
|
||||
Users,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react'
|
||||
|
||||
export type NavItem = {
|
||||
label: string
|
||||
href: string
|
||||
}
|
||||
|
||||
export type NavSection = {
|
||||
id: string
|
||||
title: string
|
||||
titleEn: string
|
||||
description: string
|
||||
href: string
|
||||
icon: LucideIcon
|
||||
items: NavItem[]
|
||||
}
|
||||
|
||||
export type ProductTile = {
|
||||
id: string
|
||||
title: string
|
||||
titleEn: string
|
||||
description: string
|
||||
href: string
|
||||
icon: LucideIcon
|
||||
}
|
||||
|
||||
export const productTiles: ProductTile[] = [
|
||||
{
|
||||
id: 'products-list',
|
||||
title: 'محصولات من',
|
||||
titleEn: 'My Products',
|
||||
description: 'مشاهده و مدیریت همه محصولات فروشگاه',
|
||||
href: '/products/list',
|
||||
icon: CakeSlice,
|
||||
},
|
||||
{
|
||||
id: 'products-new',
|
||||
title: 'ثبت محصول جدید',
|
||||
titleEn: 'New Product',
|
||||
description: 'افزودن شیرینی یا محصول تازه به فروشگاه',
|
||||
href: '/products/new',
|
||||
icon: PlusCircle,
|
||||
},
|
||||
{
|
||||
id: 'products-categories',
|
||||
title: 'دسته بندی محصولات',
|
||||
titleEn: 'Categories',
|
||||
description: 'مدیریت دستهبندیهای محصولات',
|
||||
href: '/products/categories',
|
||||
icon: FolderTree,
|
||||
},
|
||||
]
|
||||
|
||||
export const navSections: NavSection[] = [
|
||||
{
|
||||
id: 'products',
|
||||
title: 'محصولات من',
|
||||
titleEn: 'Products',
|
||||
description: 'مدیریت شیرینیها و محصولات فروشگاه',
|
||||
href: '/products',
|
||||
icon: CakeSlice,
|
||||
items: productTiles.map((tile) => ({
|
||||
label: tile.title,
|
||||
href: tile.href,
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: 'users',
|
||||
title: 'کاربران',
|
||||
titleEn: 'Users',
|
||||
description: 'مدیریت کاربران، نقشها و دسترسیها',
|
||||
href: '/users',
|
||||
icon: Users,
|
||||
items: [
|
||||
{ label: 'همه کاربران', href: '/users' },
|
||||
{ label: 'ثبت کاربر جدید', href: '/users' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'orders',
|
||||
title: 'سفارشات من',
|
||||
titleEn: 'Orders',
|
||||
description: 'پیگیری و مدیریت سفارشها',
|
||||
href: '/orders',
|
||||
icon: Package,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
title: 'تنظیمات',
|
||||
titleEn: 'Settings',
|
||||
description: 'تنظیمات فروشگاه و حساب کاربری',
|
||||
href: '/settings',
|
||||
icon: Settings,
|
||||
items: [],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,91 @@
|
||||
import { products, type Product } from '../data/products'
|
||||
import { users, formatCellNumber, type User } from '../data/users'
|
||||
import { flavors } from '../data/flavors'
|
||||
import { getSelectableCategoryOptions } from './categoryOptionsStore'
|
||||
|
||||
export type SearchUserResult = {
|
||||
id: string
|
||||
name: string
|
||||
phone: string
|
||||
phoneDisplay: string
|
||||
}
|
||||
|
||||
export type ProductOptionChoice = {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
}
|
||||
|
||||
function delay(ms = 280) {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
/** Simulated API user search (`GET /users?q=`). */
|
||||
export async function searchUsers(query: string): Promise<SearchUserResult[]> {
|
||||
await delay()
|
||||
const q = query.trim().toLowerCase()
|
||||
const digits = q.replace(/\D/g, '')
|
||||
|
||||
return users
|
||||
.filter((user) => !user.disabled)
|
||||
.filter((user) => {
|
||||
if (!q) return true
|
||||
const fullName = `${user.firstName} ${user.lastName}`
|
||||
return (
|
||||
fullName.includes(query.trim()) ||
|
||||
user.firstName.includes(query.trim()) ||
|
||||
user.lastName.includes(query.trim()) ||
|
||||
(digits.length > 0 && user.cellNumber.includes(digits))
|
||||
)
|
||||
})
|
||||
.slice(0, 12)
|
||||
.map((user) => ({
|
||||
id: user.id,
|
||||
name: `${user.firstName} ${user.lastName}`,
|
||||
phone: user.cellNumber,
|
||||
phoneDisplay: formatCellNumber(user.cellNumber),
|
||||
}))
|
||||
}
|
||||
|
||||
/** Simulated API product search (`GET /products?q=`). */
|
||||
export async function searchProducts(query: string): Promise<Product[]> {
|
||||
await delay()
|
||||
const q = query.trim().toLowerCase()
|
||||
if (!q) return products.slice(0, 12)
|
||||
|
||||
return products
|
||||
.filter(
|
||||
(product) =>
|
||||
product.nameFa.includes(query.trim()) ||
|
||||
product.nameEn.toLowerCase().includes(q) ||
|
||||
product.category.includes(query.trim()),
|
||||
)
|
||||
.slice(0, 12)
|
||||
}
|
||||
|
||||
function flavorLabel(flavorId: string) {
|
||||
return flavors.find((item) => item.id === flavorId)?.nameFa ?? flavorId
|
||||
}
|
||||
|
||||
/** Options available when adding a product to an order. */
|
||||
export function getProductOrderOptions(product: Product): ProductOptionChoice[] {
|
||||
if (product.options.length > 0) {
|
||||
return product.options.map((option) => ({
|
||||
id: option.id,
|
||||
name: `${flavorLabel(option.flavorId)} — ${option.amount}`,
|
||||
price: option.price,
|
||||
}))
|
||||
}
|
||||
|
||||
return getSelectableCategoryOptions(product.category).flatMap((block) =>
|
||||
block.entries.map((entry) => ({
|
||||
id: entry.id,
|
||||
name: `${flavorLabel(block.flavorId)} — ${entry.amount}`,
|
||||
price: entry.price,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
export function findUserById(id: string): User | undefined {
|
||||
return users.find((user) => user.id === id)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { ShippingAddress } from '../data/orders'
|
||||
|
||||
const STORAGE_KEY = 'balout-user-addresses'
|
||||
|
||||
type AddressMap = Record<string, ShippingAddress[]>
|
||||
|
||||
function uid() {
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
|
||||
}
|
||||
|
||||
function seedForUser(userId: string): ShippingAddress[] {
|
||||
const seeds: Record<string, ShippingAddress[]> = {
|
||||
'1': [
|
||||
{
|
||||
id: 'u1-a1',
|
||||
name: 'منزل',
|
||||
district: 'خیابان ارم',
|
||||
address: 'قم، خیابان ارم، کوچه ۵، پلاک ۱۲',
|
||||
landline: '02531234567',
|
||||
},
|
||||
{
|
||||
id: 'u1-a2',
|
||||
name: 'مطب',
|
||||
district: 'بلوار امین',
|
||||
address: 'قم، بلوار امین، ساختمان پزشکان، طبقه ۳',
|
||||
landline: '02537654321',
|
||||
},
|
||||
],
|
||||
'2': [
|
||||
{
|
||||
id: 'u2-a1',
|
||||
name: 'منزل',
|
||||
district: 'صفاییه',
|
||||
address: 'قم، صفاییه، خیابان شهید بهشتی، پلاک ۴۵',
|
||||
landline: '02531112233',
|
||||
},
|
||||
],
|
||||
}
|
||||
return (
|
||||
seeds[userId] ?? [
|
||||
{
|
||||
id: `${userId}-default`,
|
||||
name: 'منزل',
|
||||
district: 'حرم',
|
||||
address: 'قم، اطراف حرم، پلاک ۱۰',
|
||||
landline: '',
|
||||
},
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
function readMap(): AddressMap {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return {}
|
||||
return JSON.parse(raw) as AddressMap
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function writeMap(map: AddressMap) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(map))
|
||||
}
|
||||
|
||||
export function getUserAddresses(userId: string): ShippingAddress[] {
|
||||
const map = readMap()
|
||||
if (!map[userId]) {
|
||||
map[userId] = seedForUser(userId)
|
||||
writeMap(map)
|
||||
}
|
||||
return map[userId]
|
||||
}
|
||||
|
||||
export function addUserAddress(
|
||||
userId: string,
|
||||
input: Omit<ShippingAddress, 'id'>,
|
||||
): ShippingAddress {
|
||||
const map = readMap()
|
||||
const list = map[userId] ?? seedForUser(userId)
|
||||
const address: ShippingAddress = { ...input, id: uid() }
|
||||
map[userId] = [...list, address]
|
||||
writeMap(map)
|
||||
return address
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import type {
|
||||
User,
|
||||
UserCategory,
|
||||
UserRole,
|
||||
UserTitle,
|
||||
} from '../data/users'
|
||||
import { apiRequest } from './api'
|
||||
|
||||
export type UserListResponse = {
|
||||
items: User[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export type ListUsersParams = {
|
||||
q?: string
|
||||
role?: UserRole | ''
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export type CreateUserPayload = {
|
||||
title: UserTitle
|
||||
firstName: string
|
||||
lastName: string
|
||||
category?: UserCategory | null
|
||||
cellNumber: string
|
||||
password: string
|
||||
role?: UserRole
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export type UpdateUserPayload = {
|
||||
title?: UserTitle
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
category?: UserCategory | null
|
||||
cellNumber?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
function buildQuery(params: ListUsersParams) {
|
||||
const search = new URLSearchParams()
|
||||
if (params.q?.trim()) search.set('q', params.q.trim())
|
||||
if (params.role) search.set('role', params.role)
|
||||
if (params.page) search.set('page', String(params.page))
|
||||
if (params.pageSize) search.set('pageSize', String(params.pageSize))
|
||||
const qs = search.toString()
|
||||
return qs ? `?${qs}` : ''
|
||||
}
|
||||
|
||||
export function listUsers(params: ListUsersParams = {}) {
|
||||
return apiRequest<UserListResponse>(`/users${buildQuery(params)}`)
|
||||
}
|
||||
|
||||
export function createUser(payload: CreateUserPayload) {
|
||||
return apiRequest<User>('/users', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
...payload,
|
||||
category: payload.category ?? undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateUser(id: string, payload: UpdateUserPayload) {
|
||||
return apiRequest<User>(`/users/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export function updateUserRole(id: string, role: UserRole) {
|
||||
return apiRequest<User>(`/users/${id}/role`, {
|
||||
method: 'PATCH',
|
||||
body: { role },
|
||||
})
|
||||
}
|
||||
|
||||
export function updateUserPassword(id: string, password: string) {
|
||||
return apiRequest<User>(`/users/${id}/password`, {
|
||||
method: 'PATCH',
|
||||
body: { password },
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteUser(id: string) {
|
||||
return apiRequest<{ ok: true }>(`/users/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user