fix some bugs
This commit is contained in:
+17
-3
@@ -7,7 +7,11 @@ import {
|
||||
sendOtp,
|
||||
verifyOtp,
|
||||
updateProfile as authUpdateProfile,
|
||||
fetchMe,
|
||||
} from '@/services/authService'
|
||||
import { useCartStore } from './cart'
|
||||
import { useOrderStore } from './order'
|
||||
import { useCatalogStore } from './catalog'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<User | null>(getCurrentUser())
|
||||
@@ -42,6 +46,11 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
loading.value = false
|
||||
if (result.success && result.user) {
|
||||
user.value = result.user
|
||||
await Promise.all([
|
||||
useCatalogStore().loadCompanies(),
|
||||
useCartStore().hydrate(),
|
||||
useOrderStore().hydrate(),
|
||||
])
|
||||
} else {
|
||||
error.value = result.message
|
||||
}
|
||||
@@ -51,17 +60,22 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
function logout() {
|
||||
authLogout()
|
||||
user.value = null
|
||||
useCartStore().resetLocal()
|
||||
useOrderStore().resetLocal()
|
||||
}
|
||||
|
||||
function updateProfile(data: Partial<Pick<User, 'firstName' | 'lastName' | 'nationalId'>>) {
|
||||
async function updateProfile(data: Partial<Pick<User, 'firstName' | 'lastName' | 'nationalId'>>) {
|
||||
if (!user.value) return null
|
||||
const updated = authUpdateProfile(user.value.id, data)
|
||||
const updated = await authUpdateProfile(data)
|
||||
if (updated) user.value = updated
|
||||
return updated
|
||||
}
|
||||
|
||||
function hydrate() {
|
||||
async function hydrate() {
|
||||
user.value = getCurrentUser()
|
||||
if (!user.value) return
|
||||
const me = await fetchMe()
|
||||
user.value = me
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+135
-166
@@ -1,31 +1,22 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Cart, CartItem, Company, Product } from '@/types'
|
||||
import { generateId } from '@/utils/format'
|
||||
import type { Cart, Company, Product } from '@/types'
|
||||
import { validateMeterage } from '@/utils/validation'
|
||||
import { getItem, setItem, STORAGE_KEYS } from '@/services/storage'
|
||||
import { getCompanyById } from '@/data/mockData'
|
||||
import { apiRequest, ApiError } from '@/services/http'
|
||||
import { getAccessToken } from '@/services/http'
|
||||
import { useCatalogStore } from './catalog'
|
||||
|
||||
const DEFAULT_MAX_METERAGE = 5000
|
||||
|
||||
function loadCarts(): Cart[] {
|
||||
return getItem<Cart[]>(STORAGE_KEYS.carts, [])
|
||||
type ApiCart = Cart & { totalMeterage?: number; itemCount?: number }
|
||||
|
||||
function emptyCart(): Cart {
|
||||
return { id: 'local', companyId: '', companyName: '', items: [] }
|
||||
}
|
||||
|
||||
function saveCarts(carts: Cart[]) {
|
||||
setItem(STORAGE_KEYS.carts, carts)
|
||||
}
|
||||
|
||||
function loadActiveCartId(): string | null {
|
||||
return getItem<string | null>(STORAGE_KEYS.activeCartId, null)
|
||||
}
|
||||
|
||||
function saveActiveCartId(id: string | null) {
|
||||
setItem(STORAGE_KEYS.activeCartId, id)
|
||||
}
|
||||
|
||||
function getCartTotalMeterage(cart: Cart): number {
|
||||
return cart.items.reduce((sum, item) => sum + item.meterage, 0)
|
||||
function uniqueCompanyNames(items: Cart['items']): string {
|
||||
const names = [...new Set(items.map((item) => item.companyName).filter(Boolean))]
|
||||
return names.join('، ')
|
||||
}
|
||||
|
||||
function companyMeterage(cart: Cart, companyId: string): number {
|
||||
@@ -34,35 +25,13 @@ function companyMeterage(cart: Cart, companyId: string): number {
|
||||
.reduce((sum, item) => sum + item.meterage, 0)
|
||||
}
|
||||
|
||||
function uniqueCompanyNames(items: CartItem[]): string {
|
||||
const names = [...new Set(items.map((item) => item.companyName).filter(Boolean))]
|
||||
return names.join('، ')
|
||||
}
|
||||
|
||||
function syncCartCompanyMeta(cart: Cart) {
|
||||
if (cart.items.length === 0) return
|
||||
const first = cart.items[0]!
|
||||
cart.companyId = first.companyId
|
||||
cart.companyName = uniqueCompanyNames(cart.items) || first.companyName
|
||||
}
|
||||
|
||||
function migrateCartItems(carts: Cart[]): Cart[] {
|
||||
return carts.map((cart) => ({
|
||||
...cart,
|
||||
items: cart.items.map((item) => ({
|
||||
...item,
|
||||
companyId: item.companyId || cart.companyId,
|
||||
companyName: item.companyName || cart.companyName,
|
||||
})),
|
||||
}))
|
||||
}
|
||||
|
||||
export const useCartStore = defineStore('cart', () => {
|
||||
const carts = ref<Cart[]>(migrateCartItems(loadCarts()))
|
||||
const activeCartId = ref<string | null>(loadActiveCartId())
|
||||
const carts = ref<Cart[]>([emptyCart()])
|
||||
const activeCartId = ref<string | null>('local')
|
||||
const selectedCompanyId = ref<string | null>(null)
|
||||
const syncing = ref(false)
|
||||
|
||||
const activeCart = computed(() => carts.value.find((c) => c.id === activeCartId.value) ?? 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,
|
||||
@@ -75,107 +44,101 @@ export const useCartStore = defineStore('cart', () => {
|
||||
return uniqueCompanyNames(activeCart.value.items)
|
||||
})
|
||||
|
||||
const catalogStore = useCatalogStore()
|
||||
|
||||
const maxMeterage = computed(() => {
|
||||
if (!activeCart.value?.items.length) {
|
||||
const selected = selectedCompanyId.value
|
||||
? getCompanyById(selectedCompanyId.value)
|
||||
? catalogStore.getCompany(selectedCompanyId.value)
|
||||
: null
|
||||
return selected?.maxMeteragePerInvoice ?? DEFAULT_MAX_METERAGE
|
||||
}
|
||||
|
||||
const companyIds = [...new Set(activeCart.value.items.map((item) => item.companyId))]
|
||||
return companyIds.reduce((sum, id) => {
|
||||
const company = getCompanyById(id)
|
||||
const company = catalogStore.getCompany(id)
|
||||
return sum + (company?.maxMeteragePerInvoice ?? DEFAULT_MAX_METERAGE)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
const remainingCapacity = computed(() => Math.max(0, maxMeterage.value - totalMeterage.value))
|
||||
|
||||
const isFull = computed(() => totalMeterage.value >= maxMeterage.value)
|
||||
|
||||
function persist() {
|
||||
saveCarts(carts.value)
|
||||
saveActiveCartId(activeCartId.value)
|
||||
}
|
||||
|
||||
function createEmptyCart(company?: Company): Cart {
|
||||
function applyServerCart(serverCart: ApiCart) {
|
||||
const cart: Cart = {
|
||||
id: generateId(),
|
||||
companyId: company?.id ?? '',
|
||||
companyName: company?.name ?? '',
|
||||
items: [],
|
||||
id: serverCart.id,
|
||||
companyId: serverCart.companyId || '',
|
||||
companyName: serverCart.companyName || uniqueCompanyNames(serverCart.items),
|
||||
items: serverCart.items ?? [],
|
||||
notes: serverCart.notes,
|
||||
}
|
||||
carts.value.push(cart)
|
||||
carts.value = [cart]
|
||||
activeCartId.value = cart.id
|
||||
if (company) selectedCompanyId.value = company.id
|
||||
persist()
|
||||
return cart
|
||||
if (cart.companyId) selectedCompanyId.value = cart.companyId
|
||||
}
|
||||
|
||||
function getOrCreateCart(company?: Company): Cart {
|
||||
if (activeCart.value) return activeCart.value
|
||||
|
||||
const withItems = carts.value.find((cart) => cart.items.length > 0)
|
||||
if (withItems) {
|
||||
activeCartId.value = withItems.id
|
||||
selectedCompanyId.value = selectedCompanyId.value ?? withItems.companyId
|
||||
persist()
|
||||
return withItems
|
||||
async function hydrate() {
|
||||
if (!getAccessToken()) {
|
||||
resetLocal()
|
||||
return
|
||||
}
|
||||
|
||||
const open = carts.value.find(
|
||||
(cart) => getCartTotalMeterage(cart) < DEFAULT_MAX_METERAGE,
|
||||
)
|
||||
if (open) {
|
||||
activeCartId.value = open.id
|
||||
if (company) selectedCompanyId.value = company.id
|
||||
persist()
|
||||
return open
|
||||
syncing.value = true
|
||||
try {
|
||||
const serverCart = await apiRequest<ApiCart>('/carts')
|
||||
applyServerCart(serverCart)
|
||||
} catch {
|
||||
resetLocal()
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
|
||||
return createEmptyCart(company)
|
||||
}
|
||||
|
||||
/** @deprecated Prefer getOrCreateCart / selectCompany — kept for callers. */
|
||||
function initCartForCompany(company: Company): Cart {
|
||||
selectedCompanyId.value = company.id
|
||||
const cart = getOrCreateCart(company)
|
||||
if (!cart.companyId) {
|
||||
cart.companyId = company.id
|
||||
cart.companyName = company.name
|
||||
persist()
|
||||
}
|
||||
return cart
|
||||
function resetLocal() {
|
||||
carts.value = [emptyCart()]
|
||||
activeCartId.value = carts.value[0]!.id
|
||||
selectedCompanyId.value = null
|
||||
}
|
||||
|
||||
function selectCompany(company: Company): void {
|
||||
selectedCompanyId.value = company.id
|
||||
getOrCreateCart(company)
|
||||
if (activeCart.value && !activeCart.value.items.length) {
|
||||
activeCart.value.companyId = company.id
|
||||
activeCart.value.companyName = company.name
|
||||
}
|
||||
}
|
||||
|
||||
function createNewCartForSameCompany(): Cart | null {
|
||||
const company = selectedCompanyId.value
|
||||
? getCompanyById(selectedCompanyId.value)
|
||||
: activeCart.value?.companyId
|
||||
? getCompanyById(activeCart.value.companyId)
|
||||
: undefined
|
||||
return createEmptyCart(company)
|
||||
function initCartForCompany(company: Company): Cart {
|
||||
selectCompany(company)
|
||||
return activeCart.value ?? emptyCart()
|
||||
}
|
||||
|
||||
function addItem(
|
||||
function getOrCreateCart(company?: Company): Cart {
|
||||
if (company) selectCompany(company)
|
||||
return activeCart.value ?? emptyCart()
|
||||
}
|
||||
|
||||
async function createNewCartForSameCompany(): Promise<Cart | null> {
|
||||
try {
|
||||
const serverCart = await apiRequest<ApiCart>('/carts', { method: 'DELETE' })
|
||||
applyServerCart(serverCart)
|
||||
return activeCart.value
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function addItem(
|
||||
product: Product,
|
||||
catalogCode: string,
|
||||
meterage: number,
|
||||
): { success: boolean; message?: string } {
|
||||
const company = getCompanyById(product.companyId)
|
||||
): Promise<{ success: boolean; message?: string }> {
|
||||
const company = catalogStore.getCompany(product.companyId)
|
||||
if (!company) {
|
||||
return { success: false, message: 'شرکت محصول یافت نشد.' }
|
||||
}
|
||||
|
||||
const cart = getOrCreateCart(company)
|
||||
selectedCompanyId.value = company.id
|
||||
|
||||
const companyMax = company.maxMeteragePerInvoice
|
||||
const currentCompanyMeterage = companyMeterage(cart, company.id)
|
||||
const existing = cart.items.find((i) => i.productId === product.id)
|
||||
@@ -192,41 +155,40 @@ export const useCartStore = defineStore('cart', () => {
|
||||
return { success: false, message: validation.message }
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
existing.meterage = meterage
|
||||
existing.catalogCode = catalogCode
|
||||
existing.companyId = company.id
|
||||
existing.companyName = company.name
|
||||
} else {
|
||||
cart.items.push({
|
||||
id: generateId(),
|
||||
productId: product.id,
|
||||
catalogCode,
|
||||
productName: product.name,
|
||||
productCode: product.code,
|
||||
color: product.color,
|
||||
meterage,
|
||||
unitPrice: product.basePrice,
|
||||
companyId: company.id,
|
||||
companyName: company.name,
|
||||
})
|
||||
try {
|
||||
let serverCart: ApiCart
|
||||
if (existing) {
|
||||
serverCart = await apiRequest<ApiCart>(`/carts/items/${existing.id}`, {
|
||||
method: 'PATCH',
|
||||
body: { meterage },
|
||||
})
|
||||
} else {
|
||||
serverCart = await apiRequest<ApiCart>('/carts/items', {
|
||||
method: 'POST',
|
||||
body: { productId: product.id, meterage },
|
||||
})
|
||||
}
|
||||
applyServerCart(serverCart)
|
||||
selectedCompanyId.value = company.id
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
message: e instanceof ApiError ? e.message : 'افزودن به سبد ناموفق بود.',
|
||||
}
|
||||
}
|
||||
|
||||
syncCartCompanyMeta(cart)
|
||||
persist()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
function updateItemMeterage(
|
||||
async function updateItemMeterage(
|
||||
itemId: string,
|
||||
meterage: number,
|
||||
product: Product,
|
||||
): { success: boolean; message?: string } {
|
||||
): Promise<{ success: boolean; message?: string }> {
|
||||
if (!activeCart.value) return { success: false, message: 'سبد خرید یافت نشد.' }
|
||||
const item = activeCart.value.items.find((i) => i.id === itemId)
|
||||
if (!item) return { success: false, message: 'آیتم یافت نشد.' }
|
||||
|
||||
const company = getCompanyById(item.companyId || product.companyId)
|
||||
const company = catalogStore.getCompany(item.companyId || product.companyId)
|
||||
const companyMax = company?.maxMeteragePerInvoice ?? DEFAULT_MAX_METERAGE
|
||||
const currentCompanyMeterage = companyMeterage(activeCart.value, item.companyId)
|
||||
|
||||
@@ -241,56 +203,62 @@ export const useCartStore = defineStore('cart', () => {
|
||||
return { success: false, message: validation.message }
|
||||
}
|
||||
|
||||
item.meterage = meterage
|
||||
persist()
|
||||
return { success: true }
|
||||
try {
|
||||
const serverCart = await apiRequest<ApiCart>(`/carts/items/${itemId}`, {
|
||||
method: 'PATCH',
|
||||
body: { meterage },
|
||||
})
|
||||
applyServerCart(serverCart)
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
message: e instanceof ApiError ? e.message : 'بهروزرسانی متراژ ناموفق بود.',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeItem(itemId: string) {
|
||||
if (!activeCart.value) return
|
||||
activeCart.value.items = activeCart.value.items.filter((i) => i.id !== itemId)
|
||||
syncCartCompanyMeta(activeCart.value)
|
||||
persist()
|
||||
async function removeItem(itemId: string) {
|
||||
try {
|
||||
const serverCart = await apiRequest<ApiCart>(`/carts/items/${itemId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
applyServerCart(serverCart)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function setNotes(notes: string) {
|
||||
if (!activeCart.value) return
|
||||
activeCart.value.notes = notes
|
||||
persist()
|
||||
async function setNotes(notes: string) {
|
||||
try {
|
||||
const serverCart = await apiRequest<ApiCart>('/carts/notes', {
|
||||
method: 'PATCH',
|
||||
body: { notes },
|
||||
})
|
||||
applyServerCart(serverCart)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function clearActiveCart() {
|
||||
if (!activeCart.value) return
|
||||
activeCart.value.items = []
|
||||
activeCart.value.notes = undefined
|
||||
persist()
|
||||
async function clearActiveCart() {
|
||||
try {
|
||||
const serverCart = await apiRequest<ApiCart>('/carts', { method: 'DELETE' })
|
||||
applyServerCart(serverCart)
|
||||
} catch {
|
||||
resetLocal()
|
||||
}
|
||||
}
|
||||
|
||||
function ensureActiveCartForCartPage() {
|
||||
if (activeCart.value?.items.length) return
|
||||
|
||||
const cartWithItems = carts.value.find((cart) => cart.items.length > 0)
|
||||
if (!cartWithItems) return
|
||||
|
||||
activeCartId.value = cartWithItems.id
|
||||
selectedCompanyId.value = selectedCompanyId.value || cartWithItems.companyId
|
||||
persist()
|
||||
}
|
||||
|
||||
function hydrate() {
|
||||
carts.value = migrateCartItems(loadCarts())
|
||||
activeCartId.value = loadActiveCartId()
|
||||
if (activeCart.value) {
|
||||
syncCartCompanyMeta(activeCart.value)
|
||||
selectedCompanyId.value = selectedCompanyId.value || activeCart.value.companyId
|
||||
persist()
|
||||
}
|
||||
// server keeps a single cart
|
||||
}
|
||||
|
||||
return {
|
||||
carts,
|
||||
activeCartId,
|
||||
selectedCompanyId,
|
||||
syncing,
|
||||
activeCart,
|
||||
totalMeterage,
|
||||
itemCount,
|
||||
@@ -308,5 +276,6 @@ export const useCartStore = defineStore('cart', () => {
|
||||
clearActiveCart,
|
||||
ensureActiveCartForCartPage,
|
||||
hydrate,
|
||||
resetLocal,
|
||||
}
|
||||
})
|
||||
|
||||
+81
-18
@@ -1,38 +1,86 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
companies as mockCompanies,
|
||||
getCatalogsByCompany,
|
||||
getProductsByCatalog,
|
||||
getCatalogById,
|
||||
getProductById,
|
||||
getCompanyById,
|
||||
} from '@/data/mockData'
|
||||
import type { Catalog, Company, Product } from '@/types'
|
||||
import { apiRequest } from '@/services/http'
|
||||
import type { BankAccount, Catalog, Company, Product } from '@/types'
|
||||
|
||||
export const useCatalogStore = defineStore('catalog', () => {
|
||||
const companies = ref<Company[]>(mockCompanies.filter((c) => c.isActive))
|
||||
const companies = ref<Company[]>([])
|
||||
const catalogs = ref<Catalog[]>([])
|
||||
const products = ref<Product[]>([])
|
||||
const bankAccounts = ref<BankAccount[]>([])
|
||||
const searchQuery = ref('')
|
||||
const loading = ref(false)
|
||||
const loaded = ref(false)
|
||||
|
||||
async function loadCompanies(force = false) {
|
||||
if (loaded.value && !force) return companies.value
|
||||
loading.value = true
|
||||
try {
|
||||
const list = await apiRequest<Company[]>('/companies', {
|
||||
query: { activeOnly: true },
|
||||
})
|
||||
companies.value = list
|
||||
.filter((c) => c.isActive)
|
||||
.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
loaded.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
return companies.value
|
||||
}
|
||||
|
||||
function getActiveCompanies(): Company[] {
|
||||
return [...companies.value].sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
}
|
||||
|
||||
function getCompany(id: string): Company | undefined {
|
||||
return getCompanyById(id)
|
||||
return companies.value.find((c) => c.id === id)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function fetchCatalogs(companyId: string, query = ''): Catalog[] {
|
||||
let list = getCatalogsByCompany(companyId)
|
||||
let list = catalogs.value.filter((c) => c.companyId === companyId && c.isActive)
|
||||
if (query.trim()) {
|
||||
const q = query.trim()
|
||||
list = list.filter((c) => c.code.includes(q) || c.name.includes(q))
|
||||
}
|
||||
return list
|
||||
return [...list].sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
}
|
||||
|
||||
function fetchProducts(catalogId: string, colorFilter?: string): Product[] {
|
||||
let list = getProductsByCatalog(catalogId)
|
||||
let list = products.value.filter((p) => p.catalogId === catalogId && p.isActive)
|
||||
if (colorFilter && colorFilter !== 'all') {
|
||||
list = list.filter((p) => p.color.includes(colorFilter))
|
||||
}
|
||||
@@ -40,21 +88,35 @@ export const useCatalogStore = defineStore('catalog', () => {
|
||||
}
|
||||
|
||||
function getCatalog(id: string) {
|
||||
return getCatalogById(id)
|
||||
return catalogs.value.find((c) => c.id === id)
|
||||
}
|
||||
|
||||
function getProduct(id: string) {
|
||||
return getProductById(id)
|
||||
return products.value.find((p) => p.id === id)
|
||||
}
|
||||
|
||||
function getUniqueColors(catalogId: string): string[] {
|
||||
const products = getProductsByCatalog(catalogId)
|
||||
return [...new Set(products.map((p) => p.color))]
|
||||
return [...new Set(fetchProducts(catalogId).map((p) => p.color))]
|
||||
}
|
||||
|
||||
function getBankAccountsByCompany(companyId: string): BankAccount[] {
|
||||
return bankAccounts.value
|
||||
.filter((b) => b.companyId === companyId && b.isActive)
|
||||
.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
}
|
||||
|
||||
return {
|
||||
companies,
|
||||
catalogs,
|
||||
products,
|
||||
bankAccounts,
|
||||
searchQuery,
|
||||
loading,
|
||||
loaded,
|
||||
loadCompanies,
|
||||
loadCatalogs,
|
||||
loadProducts,
|
||||
loadBankAccounts,
|
||||
getActiveCompanies,
|
||||
getCompany,
|
||||
fetchCatalogs,
|
||||
@@ -62,5 +124,6 @@ export const useCatalogStore = defineStore('catalog', () => {
|
||||
getCatalog,
|
||||
getProduct,
|
||||
getUniqueColors,
|
||||
getBankAccountsByCompany,
|
||||
}
|
||||
})
|
||||
|
||||
+121
-154
@@ -5,87 +5,29 @@ import type {
|
||||
CheckData,
|
||||
CashPaymentReceipt,
|
||||
Order,
|
||||
OrderItem,
|
||||
OrderStatus,
|
||||
OrderStatusHistoryEntry,
|
||||
SettlementType,
|
||||
CheckRegistrationMode,
|
||||
} from '@/types'
|
||||
import { generateId, generateOrderNumber } from '@/utils/format'
|
||||
import { generateId } from '@/utils/format'
|
||||
import { calculateCheckRass } from '@/utils/checkRass'
|
||||
import { getItem, setItem, STORAGE_KEYS } from '@/services/storage'
|
||||
import { getCompanyById, getBankAccountsByCompany } from '@/data/mockData'
|
||||
|
||||
function loadOrders(): Order[] {
|
||||
return getItem<Order[]>(STORAGE_KEYS.orders, [])
|
||||
}
|
||||
|
||||
function saveOrders(orders: Order[]) {
|
||||
setItem(STORAGE_KEYS.orders, orders)
|
||||
}
|
||||
|
||||
function createStatusEntry(
|
||||
from: OrderStatus | null,
|
||||
to: OrderStatus,
|
||||
note?: string,
|
||||
): OrderStatusHistoryEntry {
|
||||
return {
|
||||
id: generateId(),
|
||||
fromStatus: from,
|
||||
toStatus: to,
|
||||
changedAt: new Date().toISOString(),
|
||||
changedBy: 'customer',
|
||||
note,
|
||||
}
|
||||
}
|
||||
|
||||
function cartToOrderItems(cart: Cart): OrderItem[] {
|
||||
return cart.items.map((item) => ({
|
||||
id: generateId(),
|
||||
productId: item.productId,
|
||||
catalogCode: item.catalogCode,
|
||||
productName: item.productName,
|
||||
productCode: item.productCode,
|
||||
color: item.color,
|
||||
requestedMeterage: item.meterage,
|
||||
unitPrice: item.unitPrice,
|
||||
notes: item.notes,
|
||||
}))
|
||||
}
|
||||
|
||||
function computeCashExpiry(companyId: string): string {
|
||||
const company = getCompanyById(companyId)
|
||||
const now = new Date()
|
||||
if (company?.cashInvoiceValidUntilHour) {
|
||||
const expiry = new Date(now)
|
||||
expiry.setHours(company.cashInvoiceValidUntilHour, 0, 0, 0)
|
||||
if (expiry <= now) {
|
||||
expiry.setDate(expiry.getDate() + 1)
|
||||
}
|
||||
return expiry.toISOString()
|
||||
}
|
||||
const hours = company?.cashInvoiceValidityHours ?? 8
|
||||
return new Date(now.getTime() + hours * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
import { apiRequest, ApiError, getAccessToken } from '@/services/http'
|
||||
import { useCatalogStore } from './catalog'
|
||||
|
||||
export const useOrderStore = defineStore('order', () => {
|
||||
const orders = ref<Order[]>(loadOrders())
|
||||
const orders = ref<Order[]>([])
|
||||
const draftChecks = ref<CheckData[]>([])
|
||||
const draftSettlementType = ref<SettlementType | null>(null)
|
||||
const draftCheckMode = ref<CheckRegistrationMode | null>(null)
|
||||
const checkoutCartId = ref<string | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const userOrders = computed(
|
||||
() => (userId: string) =>
|
||||
orders.value
|
||||
.filter((o) => o.userId === userId)
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()),
|
||||
() => (_userId: string) =>
|
||||
[...orders.value].sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
),
|
||||
)
|
||||
|
||||
function persist() {
|
||||
saveOrders(orders.value)
|
||||
}
|
||||
|
||||
function getOrderById(id: string): Order | undefined {
|
||||
return orders.value.find((o) => o.id === id)
|
||||
}
|
||||
@@ -120,108 +62,131 @@ export const useOrderStore = defineStore('order', () => {
|
||||
return calculateCheckRass(draftChecks.value, baseDate ?? new Date())
|
||||
}
|
||||
|
||||
function submitOrder(
|
||||
userId: string,
|
||||
cart: Cart,
|
||||
settlementType: SettlementType,
|
||||
checks?: CheckData[],
|
||||
checkMode?: CheckRegistrationMode,
|
||||
): Order {
|
||||
const items = cartToOrderItems(cart)
|
||||
const totalMeterage = items.reduce((s, i) => s + i.requestedMeterage, 0)
|
||||
const now = new Date().toISOString()
|
||||
|
||||
let status: OrderStatus = 'awaiting_review'
|
||||
let checkRassDays: number | undefined
|
||||
let checkRassDate: string | undefined
|
||||
let cashInvoiceExpiresAt: string | undefined
|
||||
|
||||
if (settlementType === 'check' && checks && checks.length > 0) {
|
||||
const rass = calculateCheckRass(checks, new Date())
|
||||
checkRassDays = rass.rassDays
|
||||
checkRassDate = rass.rassDate ?? undefined
|
||||
status = 'awaiting_review'
|
||||
async function hydrate() {
|
||||
if (!getAccessToken()) {
|
||||
orders.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (settlementType === 'cash') {
|
||||
cashInvoiceExpiresAt = computeCashExpiry(cart.companyId)
|
||||
status = 'awaiting_review'
|
||||
loading.value = true
|
||||
try {
|
||||
orders.value = await apiRequest<Order[]>('/orders')
|
||||
} catch {
|
||||
orders.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const order: Order = {
|
||||
id: generateId(),
|
||||
orderNumber: generateOrderNumber(),
|
||||
userId,
|
||||
companyId: cart.companyId,
|
||||
companyName: cart.companyName,
|
||||
items,
|
||||
totalMeterage,
|
||||
settlementType,
|
||||
checkMode,
|
||||
checks: checks ? [...checks] : undefined,
|
||||
checkRassDays,
|
||||
checkRassDate,
|
||||
status,
|
||||
notes: cart.notes,
|
||||
cashInvoiceExpiresAt,
|
||||
statusHistory: [
|
||||
createStatusEntry(null, 'submitted', 'سفارش ثبت شد'),
|
||||
createStatusEntry('submitted', status, 'ارسال برای بررسی مدیریت'),
|
||||
],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
orders.value.unshift(order)
|
||||
persist()
|
||||
|
||||
function resetLocal() {
|
||||
orders.value = []
|
||||
draftChecks.value = []
|
||||
draftSettlementType.value = null
|
||||
draftCheckMode.value = null
|
||||
checkoutCartId.value = null
|
||||
|
||||
return order
|
||||
}
|
||||
|
||||
function submitCashReceipt(
|
||||
async function fetchOrder(id: string): Promise<Order | null> {
|
||||
try {
|
||||
const order = await apiRequest<Order>(`/orders/${id}`)
|
||||
const idx = orders.value.findIndex((o) => o.id === id)
|
||||
if (idx >= 0) orders.value[idx] = order
|
||||
else orders.value.unshift(order)
|
||||
return order
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function submitOrder(
|
||||
_userId: string,
|
||||
cart: Cart,
|
||||
settlementType: SettlementType,
|
||||
checks?: CheckData[],
|
||||
checkMode?: CheckRegistrationMode,
|
||||
): Promise<Order> {
|
||||
if (!cart.companyId) {
|
||||
throw new Error('شرکت سبد مشخص نیست.')
|
||||
}
|
||||
if (!cart.items.length) {
|
||||
throw new Error('سبد خرید خالی است.')
|
||||
}
|
||||
|
||||
const body = {
|
||||
companyId: cart.companyId,
|
||||
settlementType,
|
||||
checkMode: settlementType === 'check' ? (checkMode ?? 'full') : undefined,
|
||||
notes: cart.notes,
|
||||
items: cart.items.map((item) => ({
|
||||
productId: item.productId,
|
||||
meterage: item.meterage,
|
||||
notes: item.notes,
|
||||
})),
|
||||
checks:
|
||||
settlementType === 'check' && checks
|
||||
? checks.map(({ id: _id, ...rest }) => rest)
|
||||
: undefined,
|
||||
}
|
||||
|
||||
try {
|
||||
const order = await apiRequest<Order>('/orders', {
|
||||
method: 'POST',
|
||||
body,
|
||||
})
|
||||
orders.value.unshift(order)
|
||||
draftChecks.value = []
|
||||
draftSettlementType.value = null
|
||||
draftCheckMode.value = null
|
||||
checkoutCartId.value = null
|
||||
return order
|
||||
} catch (e) {
|
||||
throw new Error(e instanceof ApiError ? e.message : 'ثبت سفارش ناموفق بود.')
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCashReceipt(
|
||||
orderId: string,
|
||||
receipt: Omit<CashPaymentReceipt, 'id' | 'submittedAt'>,
|
||||
) {
|
||||
const order = getOrderById(orderId)
|
||||
if (!order) return null
|
||||
|
||||
const fullReceipt: CashPaymentReceipt = {
|
||||
...receipt,
|
||||
id: generateId(),
|
||||
submittedAt: new Date().toISOString(),
|
||||
try {
|
||||
const order = await apiRequest<Order>(`/orders/${orderId}/cash-receipt`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
amount: receipt.amount,
|
||||
transferDate: receipt.transferDate,
|
||||
transferTime: receipt.transferTime,
|
||||
trackingNumber: receipt.trackingNumber,
|
||||
destinationAccountId: receipt.destinationAccountId,
|
||||
receiptImage: receipt.receiptImage,
|
||||
notes: receipt.notes,
|
||||
},
|
||||
})
|
||||
const idx = orders.value.findIndex((o) => o.id === orderId)
|
||||
if (idx >= 0) orders.value[idx] = order
|
||||
else orders.value.unshift(order)
|
||||
return order
|
||||
} catch (e) {
|
||||
throw new Error(e instanceof ApiError ? e.message : 'ثبت رسید ناموفق بود.')
|
||||
}
|
||||
|
||||
const prevStatus = order.status
|
||||
order.cashReceipt = fullReceipt
|
||||
order.status = 'awaiting_cash_confirmation'
|
||||
order.statusHistory.push(
|
||||
createStatusEntry(prevStatus, 'awaiting_cash_confirmation', 'رسید پرداخت ثبت شد'),
|
||||
)
|
||||
order.updatedAt = new Date().toISOString()
|
||||
persist()
|
||||
return order
|
||||
}
|
||||
|
||||
function updateOrderChecks(orderId: string, checks: CheckData[]) {
|
||||
const order = getOrderById(orderId)
|
||||
if (!order) return null
|
||||
|
||||
const rass = calculateCheckRass(checks, new Date(order.createdAt))
|
||||
order.checks = checks
|
||||
order.checkRassDays = rass.rassDays
|
||||
order.checkRassDate = rass.rassDate ?? undefined
|
||||
order.updatedAt = new Date().toISOString()
|
||||
persist()
|
||||
return order
|
||||
async function updateOrderChecks(orderId: string, checks: CheckData[]) {
|
||||
try {
|
||||
const order = await apiRequest<Order>(`/orders/${orderId}/checks`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
checks: checks.map(({ id: _id, ...rest }) => rest),
|
||||
},
|
||||
})
|
||||
const idx = orders.value.findIndex((o) => o.id === orderId)
|
||||
if (idx >= 0) orders.value[idx] = order
|
||||
return order
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function hydrate() {
|
||||
orders.value = loadOrders()
|
||||
async function getBankAccountsForCompany(companyId: string) {
|
||||
return useCatalogStore().loadBankAccounts(companyId)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -230,8 +195,8 @@ export const useOrderStore = defineStore('order', () => {
|
||||
draftSettlementType,
|
||||
draftCheckMode,
|
||||
checkoutCartId,
|
||||
loading,
|
||||
userOrders,
|
||||
persist,
|
||||
getOrderById,
|
||||
startCheckout,
|
||||
setCheckMode,
|
||||
@@ -243,6 +208,8 @@ export const useOrderStore = defineStore('order', () => {
|
||||
submitCashReceipt,
|
||||
updateOrderChecks,
|
||||
hydrate,
|
||||
getBankAccountsForCompany: getBankAccountsByCompany,
|
||||
resetLocal,
|
||||
fetchOrder,
|
||||
getBankAccountsForCompany,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, watchEffect } from 'vue'
|
||||
import { usePreferredDark, useStorage } from '@vueuse/core'
|
||||
|
||||
export type ThemePreference = 'light' | 'dark' | 'system'
|
||||
|
||||
export const useThemeStore = defineStore('theme', () => {
|
||||
const preference = useStorage<ThemePreference>('ft_theme', 'system')
|
||||
const prefersDark = usePreferredDark()
|
||||
|
||||
const resolved = computed<'light' | 'dark'>(() => {
|
||||
if (preference.value === 'system') return prefersDark.value ? 'dark' : 'light'
|
||||
return preference.value
|
||||
})
|
||||
|
||||
const isDark = computed(() => resolved.value === 'dark')
|
||||
|
||||
watchEffect(() => {
|
||||
const root = document.documentElement
|
||||
root.classList.toggle('dark', isDark.value)
|
||||
root.style.colorScheme = resolved.value
|
||||
})
|
||||
|
||||
function setPreference(value: ThemePreference) {
|
||||
preference.value = value
|
||||
}
|
||||
|
||||
return {
|
||||
preference,
|
||||
resolved,
|
||||
isDark,
|
||||
setPreference,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user