Improve auth, checkout, and API client for real backend.
Wire OTP and cart/catalog to the API with Vite proxy, fix resend/register name persistence, harden order submit errors, and polish OTP and cart footer UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+37
-4
@@ -9,6 +9,11 @@ import {
|
||||
updateProfile as authUpdateProfile,
|
||||
fetchMe,
|
||||
} from '@/services/authService'
|
||||
import {
|
||||
getPendingRegister,
|
||||
setPendingRegister,
|
||||
type PendingRegister,
|
||||
} from '@/utils/pendingRegister'
|
||||
import { useCartStore } from './cart'
|
||||
import { useOrderStore } from './order'
|
||||
import { useCatalogStore } from './catalog'
|
||||
@@ -17,6 +22,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<User | null>(getCurrentUser())
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const pendingRegister = ref<PendingRegister | null>(getPendingRegister())
|
||||
|
||||
const isAuthenticated = computed(() => !!user.value)
|
||||
const fullName = computed(() =>
|
||||
@@ -27,16 +33,41 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
error.value = null
|
||||
}
|
||||
|
||||
function clearPendingRegister() {
|
||||
pendingRegister.value = null
|
||||
setPendingRegister(null)
|
||||
}
|
||||
|
||||
async function requestOtp(
|
||||
phone: string,
|
||||
options: { firstName?: string; lastName?: string; isRegister: boolean },
|
||||
) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
const result = await sendOtp(phone, options)
|
||||
loading.value = false
|
||||
if (!result.success) error.value = result.message
|
||||
return result
|
||||
|
||||
try {
|
||||
// Re-read storage on every call (survives HMR / refresh / separate tabs).
|
||||
const stored = getPendingRegister()
|
||||
pendingRegister.value = stored
|
||||
|
||||
const firstName = options.firstName?.trim() || stored?.firstName
|
||||
const lastName = options.lastName?.trim() || stored?.lastName
|
||||
|
||||
if (options.isRegister && firstName && lastName) {
|
||||
pendingRegister.value = { firstName, lastName }
|
||||
setPendingRegister(pendingRegister.value)
|
||||
}
|
||||
|
||||
const result = await sendOtp(phone, {
|
||||
isRegister: options.isRegister,
|
||||
firstName,
|
||||
lastName,
|
||||
})
|
||||
if (!result.success) error.value = result.message
|
||||
return result
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmOtp(phone: string, code: string) {
|
||||
@@ -46,6 +77,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
loading.value = false
|
||||
if (result.success && result.user) {
|
||||
user.value = result.user
|
||||
clearPendingRegister()
|
||||
await Promise.all([
|
||||
useCatalogStore().loadCompanies(),
|
||||
useCartStore().hydrate(),
|
||||
@@ -60,6 +92,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
function logout() {
|
||||
authLogout()
|
||||
user.value = null
|
||||
clearPendingRegister()
|
||||
useCartStore().resetLocal()
|
||||
useOrderStore().resetLocal()
|
||||
}
|
||||
|
||||
+90
-4
@@ -1,9 +1,8 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Cart, Company, Product } from '@/types'
|
||||
import type { Cart, CartItem, Company, Product } from '@/types'
|
||||
import { validateMeterage } from '@/utils/validation'
|
||||
import { apiRequest, ApiError } from '@/services/http'
|
||||
import { getAccessToken } from '@/services/http'
|
||||
import { apiRequest, ApiError, getAccessToken } from '@/services/http'
|
||||
import { useCatalogStore } from './catalog'
|
||||
|
||||
const DEFAULT_MAX_METERAGE = 5000
|
||||
@@ -14,6 +13,11 @@ function emptyCart(): Cart {
|
||||
return { id: 'local', companyId: '', companyName: '', items: [] }
|
||||
}
|
||||
|
||||
function useLocalCart(): boolean {
|
||||
const token = getAccessToken()
|
||||
return import.meta.env.VITE_USE_MOCK_AUTH === 'true' || !!token?.startsWith('mock.')
|
||||
}
|
||||
|
||||
function uniqueCompanyNames(items: Cart['items']): string {
|
||||
const names = [...new Set(items.map((item) => item.companyName).filter(Boolean))]
|
||||
return names.join('، ')
|
||||
@@ -31,7 +35,9 @@ export const useCartStore = defineStore('cart', () => {
|
||||
const selectedCompanyId = ref<string | null>(null)
|
||||
const syncing = ref(false)
|
||||
|
||||
const activeCart = computed(() => carts.value.find((c) => c.id === activeCartId.value) ?? carts.value[0] ?? null)
|
||||
const activeCart = computed(
|
||||
() => carts.value.find((c) => c.id === activeCartId.value) ?? carts.value[0] ?? null,
|
||||
)
|
||||
|
||||
const totalMeterage = computed(
|
||||
() => activeCart.value?.items.reduce((sum, i) => sum + i.meterage, 0) ?? 0,
|
||||
@@ -77,11 +83,21 @@ export const useCartStore = defineStore('cart', () => {
|
||||
if (cart.companyId) selectedCompanyId.value = cart.companyId
|
||||
}
|
||||
|
||||
function replaceActiveCart(cart: Cart) {
|
||||
carts.value = [{ ...cart, items: cart.items.map((item) => ({ ...item })) }]
|
||||
activeCartId.value = carts.value[0]!.id
|
||||
}
|
||||
|
||||
async function hydrate() {
|
||||
if (!getAccessToken()) {
|
||||
resetLocal()
|
||||
return
|
||||
}
|
||||
if (useLocalCart()) {
|
||||
// Keep whatever local cart already exists for mock sessions.
|
||||
syncing.value = false
|
||||
return
|
||||
}
|
||||
syncing.value = true
|
||||
try {
|
||||
const serverCart = await apiRequest<ApiCart>('/carts')
|
||||
@@ -118,6 +134,10 @@ export const useCartStore = defineStore('cart', () => {
|
||||
}
|
||||
|
||||
async function createNewCartForSameCompany(): Promise<Cart | null> {
|
||||
if (useLocalCart()) {
|
||||
resetLocal()
|
||||
return activeCart.value
|
||||
}
|
||||
try {
|
||||
const serverCart = await apiRequest<ApiCart>('/carts', { method: 'DELETE' })
|
||||
applyServerCart(serverCart)
|
||||
@@ -128,6 +148,45 @@ export const useCartStore = defineStore('cart', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function addItemLocal(
|
||||
product: Product,
|
||||
catalogCode: string,
|
||||
meterage: number,
|
||||
company: Company,
|
||||
): { success: boolean; message?: string } {
|
||||
const cart = getOrCreateCart(company)
|
||||
const existing = cart.items.find((i) => i.productId === product.id)
|
||||
const items = [...cart.items]
|
||||
|
||||
if (existing) {
|
||||
const index = items.findIndex((i) => i.id === existing.id)
|
||||
items[index] = { ...existing, meterage }
|
||||
} else {
|
||||
const item: CartItem = {
|
||||
id: `local-item-${product.id}-${Date.now()}`,
|
||||
productId: product.id,
|
||||
catalogCode,
|
||||
productName: product.name,
|
||||
productCode: product.code,
|
||||
color: product.color,
|
||||
meterage,
|
||||
companyId: company.id,
|
||||
companyName: company.name,
|
||||
}
|
||||
items.push(item)
|
||||
}
|
||||
|
||||
replaceActiveCart({
|
||||
id: cart.id || 'local',
|
||||
companyId: company.id,
|
||||
companyName: company.name,
|
||||
items,
|
||||
notes: cart.notes,
|
||||
})
|
||||
selectedCompanyId.value = company.id
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
async function addItem(
|
||||
product: Product,
|
||||
catalogCode: string,
|
||||
@@ -155,6 +214,10 @@ export const useCartStore = defineStore('cart', () => {
|
||||
return { success: false, message: validation.message }
|
||||
}
|
||||
|
||||
if (useLocalCart()) {
|
||||
return addItemLocal(product, catalogCode, meterage, company)
|
||||
}
|
||||
|
||||
try {
|
||||
let serverCart: ApiCart
|
||||
if (existing) {
|
||||
@@ -203,6 +266,14 @@ export const useCartStore = defineStore('cart', () => {
|
||||
return { success: false, message: validation.message }
|
||||
}
|
||||
|
||||
if (useLocalCart()) {
|
||||
const items = activeCart.value.items.map((entry) =>
|
||||
entry.id === itemId ? { ...entry, meterage } : entry,
|
||||
)
|
||||
replaceActiveCart({ ...activeCart.value, items })
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
try {
|
||||
const serverCart = await apiRequest<ApiCart>(`/carts/items/${itemId}`, {
|
||||
method: 'PATCH',
|
||||
@@ -219,6 +290,12 @@ export const useCartStore = defineStore('cart', () => {
|
||||
}
|
||||
|
||||
async function removeItem(itemId: string) {
|
||||
if (useLocalCart()) {
|
||||
if (!activeCart.value) return
|
||||
const items = activeCart.value.items.filter((entry) => entry.id !== itemId)
|
||||
replaceActiveCart({ ...activeCart.value, items })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const serverCart = await apiRequest<ApiCart>(`/carts/items/${itemId}`, {
|
||||
method: 'DELETE',
|
||||
@@ -230,6 +307,11 @@ export const useCartStore = defineStore('cart', () => {
|
||||
}
|
||||
|
||||
async function setNotes(notes: string) {
|
||||
if (useLocalCart()) {
|
||||
if (!activeCart.value) return
|
||||
replaceActiveCart({ ...activeCart.value, notes })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const serverCart = await apiRequest<ApiCart>('/carts/notes', {
|
||||
method: 'PATCH',
|
||||
@@ -242,6 +324,10 @@ export const useCartStore = defineStore('cart', () => {
|
||||
}
|
||||
|
||||
async function clearActiveCart() {
|
||||
if (useLocalCart()) {
|
||||
resetLocal()
|
||||
return
|
||||
}
|
||||
try {
|
||||
const serverCart = await apiRequest<ApiCart>('/carts', { method: 'DELETE' })
|
||||
applyServerCart(serverCart)
|
||||
|
||||
+107
-24
@@ -1,8 +1,16 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { apiRequest } from '@/services/http'
|
||||
import {
|
||||
bankAccounts as mockBankAccounts,
|
||||
catalogs as mockCatalogs,
|
||||
companies as mockCompanies,
|
||||
products as mockProducts,
|
||||
} from '@/data/mockData'
|
||||
import type { BankAccount, Catalog, Company, Product } from '@/types'
|
||||
|
||||
const useMockCatalog = () => import.meta.env.VITE_USE_MOCK_AUTH === 'true'
|
||||
|
||||
export const useCatalogStore = defineStore('catalog', () => {
|
||||
const companies = ref<Company[]>([])
|
||||
const catalogs = ref<Catalog[]>([])
|
||||
@@ -16,6 +24,17 @@ export const useCatalogStore = defineStore('catalog', () => {
|
||||
if (loaded.value && !force) return companies.value
|
||||
loading.value = true
|
||||
try {
|
||||
if (import.meta.env.VITE_USE_MOCK_AUTH === 'true') {
|
||||
companies.value = mockCompanies
|
||||
.filter((c) => c.isActive)
|
||||
.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
catalogs.value = mockCatalogs
|
||||
products.value = mockProducts
|
||||
bankAccounts.value = mockBankAccounts
|
||||
loaded.value = true
|
||||
return companies.value
|
||||
}
|
||||
|
||||
const list = await apiRequest<Company[]>('/companies', {
|
||||
query: { activeOnly: true },
|
||||
})
|
||||
@@ -23,6 +42,18 @@ export const useCatalogStore = defineStore('catalog', () => {
|
||||
.filter((c) => c.isActive)
|
||||
.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
loaded.value = true
|
||||
} catch (err) {
|
||||
if (useMockCatalog()) {
|
||||
companies.value = mockCompanies
|
||||
.filter((c) => c.isActive)
|
||||
.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
catalogs.value = mockCatalogs
|
||||
products.value = mockProducts
|
||||
bankAccounts.value = mockBankAccounts
|
||||
loaded.value = true
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -38,36 +69,88 @@ export const useCatalogStore = defineStore('catalog', () => {
|
||||
}
|
||||
|
||||
async function loadCatalogs(companyId: string) {
|
||||
const list = await apiRequest<Catalog[]>('/catalogs', {
|
||||
query: { companyId, activeOnly: true },
|
||||
})
|
||||
catalogs.value = [
|
||||
...catalogs.value.filter((c) => c.companyId !== companyId),
|
||||
...list,
|
||||
]
|
||||
return list
|
||||
if (import.meta.env.VITE_USE_MOCK_AUTH === 'true') {
|
||||
const list = mockCatalogs.filter((c) => c.companyId === companyId && c.isActive)
|
||||
catalogs.value = [
|
||||
...catalogs.value.filter((c) => c.companyId !== companyId),
|
||||
...list,
|
||||
]
|
||||
return list
|
||||
}
|
||||
try {
|
||||
const list = await apiRequest<Catalog[]>('/catalogs', {
|
||||
query: { companyId, activeOnly: true },
|
||||
})
|
||||
catalogs.value = [
|
||||
...catalogs.value.filter((c) => c.companyId !== companyId),
|
||||
...list,
|
||||
]
|
||||
return list
|
||||
} catch (err) {
|
||||
if (!useMockCatalog()) throw err
|
||||
const list = mockCatalogs.filter((c) => c.companyId === companyId && c.isActive)
|
||||
catalogs.value = [
|
||||
...catalogs.value.filter((c) => c.companyId !== companyId),
|
||||
...list,
|
||||
]
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProducts(catalogId: string, color?: string) {
|
||||
const list = await apiRequest<Product[]>('/products', {
|
||||
query: {
|
||||
catalogId,
|
||||
color: color && color !== 'all' ? color : undefined,
|
||||
activeOnly: true,
|
||||
},
|
||||
})
|
||||
const others = products.value.filter((p) => p.catalogId !== catalogId)
|
||||
products.value = [...others, ...list]
|
||||
return list
|
||||
if (import.meta.env.VITE_USE_MOCK_AUTH === 'true') {
|
||||
let list = mockProducts.filter((p) => p.catalogId === catalogId && p.isActive)
|
||||
if (color && color !== 'all') list = list.filter((p) => p.color.includes(color))
|
||||
const others = products.value.filter((p) => p.catalogId !== catalogId)
|
||||
products.value = [...others, ...list]
|
||||
return list
|
||||
}
|
||||
try {
|
||||
const list = await apiRequest<Product[]>('/products', {
|
||||
query: {
|
||||
catalogId,
|
||||
color: color && color !== 'all' ? color : undefined,
|
||||
activeOnly: true,
|
||||
},
|
||||
})
|
||||
const others = products.value.filter((p) => p.catalogId !== catalogId)
|
||||
products.value = [...others, ...list]
|
||||
return list
|
||||
} catch (err) {
|
||||
if (!useMockCatalog()) throw err
|
||||
let list = mockProducts.filter((p) => p.catalogId === catalogId && p.isActive)
|
||||
if (color && color !== 'all') list = list.filter((p) => p.color.includes(color))
|
||||
const others = products.value.filter((p) => p.catalogId !== catalogId)
|
||||
products.value = [...others, ...list]
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBankAccounts(companyId: string) {
|
||||
const list = await apiRequest<BankAccount[]>(`/companies/${companyId}/bank-accounts`)
|
||||
bankAccounts.value = [
|
||||
...bankAccounts.value.filter((b) => b.companyId !== companyId),
|
||||
...list.filter((b) => b.isActive),
|
||||
]
|
||||
return bankAccounts.value.filter((b) => b.companyId === companyId)
|
||||
if (import.meta.env.VITE_USE_MOCK_AUTH === 'true') {
|
||||
const list = mockBankAccounts.filter((b) => b.companyId === companyId && b.isActive)
|
||||
bankAccounts.value = [
|
||||
...bankAccounts.value.filter((b) => b.companyId !== companyId),
|
||||
...list,
|
||||
]
|
||||
return list
|
||||
}
|
||||
try {
|
||||
const list = await apiRequest<BankAccount[]>(`/companies/${companyId}/bank-accounts`)
|
||||
bankAccounts.value = [
|
||||
...bankAccounts.value.filter((b) => b.companyId !== companyId),
|
||||
...list.filter((b) => b.isActive),
|
||||
]
|
||||
return bankAccounts.value.filter((b) => b.companyId === companyId)
|
||||
} catch (err) {
|
||||
if (!useMockCatalog()) throw err
|
||||
const list = mockBankAccounts.filter((b) => b.companyId === companyId && b.isActive)
|
||||
bankAccounts.value = [
|
||||
...bankAccounts.value.filter((b) => b.companyId !== companyId),
|
||||
...list,
|
||||
]
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
function fetchCatalogs(companyId: string, query = ''): Catalog[] {
|
||||
|
||||
+8
-2
@@ -104,21 +104,27 @@ export const useOrderStore = defineStore('order', () => {
|
||||
checks?: CheckData[],
|
||||
checkMode?: CheckRegistrationMode,
|
||||
): Promise<Order> {
|
||||
if (!cart.companyId) {
|
||||
const companyId = cart.companyId || cart.items[0]?.companyId || ''
|
||||
if (!companyId) {
|
||||
throw new Error('شرکت سبد مشخص نیست.')
|
||||
}
|
||||
if (!cart.items.length) {
|
||||
throw new Error('سبد خرید خالی است.')
|
||||
}
|
||||
if (cart.id === 'local' || cart.items.some((item) => item.id.startsWith('local-item-'))) {
|
||||
throw new Error('سبد خرید با سرور همگام نیست. لطفاً دوباره محصولات را به سبد اضافه کنید.')
|
||||
}
|
||||
|
||||
const body = {
|
||||
companyId: cart.companyId,
|
||||
companyId,
|
||||
settlementType,
|
||||
settlementMethod: settlementType,
|
||||
checkMode: settlementType === 'check' ? (checkMode ?? 'full') : undefined,
|
||||
notes: cart.notes,
|
||||
items: cart.items.map((item) => ({
|
||||
productId: item.productId,
|
||||
meterage: item.meterage,
|
||||
requestedMeterage: item.meterage,
|
||||
notes: item.notes,
|
||||
})),
|
||||
checks:
|
||||
|
||||
Reference in New Issue
Block a user