Initialize the user app and load home banners from the API.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { User } from '@/types'
|
||||
import {
|
||||
getCurrentUser,
|
||||
logout as authLogout,
|
||||
sendOtp,
|
||||
verifyOtp,
|
||||
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'
|
||||
import { useNotificationStore } from './notification'
|
||||
import { useFavoritesStore } from './favorites'
|
||||
|
||||
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(() =>
|
||||
user.value ? `${user.value.firstName} ${user.value.lastName}` : '',
|
||||
)
|
||||
const isAgent = computed(() => user.value?.appRole === 'agent')
|
||||
const isCustomerOnly = computed(
|
||||
() => !!user.value && user.value.appRole !== 'agent',
|
||||
)
|
||||
|
||||
function clearError() {
|
||||
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
|
||||
|
||||
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) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
const result = await verifyOtp(phone, code)
|
||||
loading.value = false
|
||||
if (result.success && result.user) {
|
||||
user.value = result.user
|
||||
clearPendingRegister()
|
||||
await Promise.all([
|
||||
useCatalogStore().loadCompanies(),
|
||||
useCartStore().hydrate(),
|
||||
useOrderStore().hydrate(),
|
||||
useNotificationStore().hydrate(),
|
||||
useFavoritesStore().hydrate(),
|
||||
])
|
||||
} else {
|
||||
error.value = result.message
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function logout() {
|
||||
authLogout()
|
||||
user.value = null
|
||||
clearPendingRegister()
|
||||
useCartStore().resetLocal()
|
||||
useOrderStore().resetLocal()
|
||||
useNotificationStore().resetLocal()
|
||||
useFavoritesStore().resetLocal()
|
||||
}
|
||||
|
||||
async function updateProfile(
|
||||
data: Partial<Pick<User, 'firstName' | 'lastName' | 'nationalId' | 'avatarUrl'>>,
|
||||
) {
|
||||
if (!user.value) return null
|
||||
const updated = await authUpdateProfile(data)
|
||||
if (updated) user.value = updated
|
||||
return updated
|
||||
}
|
||||
|
||||
async function hydrate() {
|
||||
user.value = getCurrentUser()
|
||||
if (!user.value) return
|
||||
const me = await fetchMe()
|
||||
user.value = me
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
loading,
|
||||
error,
|
||||
isAuthenticated,
|
||||
fullName,
|
||||
isAgent,
|
||||
isCustomerOnly,
|
||||
clearError,
|
||||
requestOtp,
|
||||
confirmOtp,
|
||||
logout,
|
||||
updateProfile,
|
||||
hydrate,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,407 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Cart, CartItem, Company, Product } from '@/types'
|
||||
import { validateMeterage } from '@/utils/validation'
|
||||
import { apiRequest, ApiError, getAccessToken } from '@/services/http'
|
||||
import { useCatalogStore } from './catalog'
|
||||
|
||||
const DEFAULT_MAX_METERAGE = 5000
|
||||
|
||||
type ApiCart = Cart & { totalMeterage?: number; itemCount?: number }
|
||||
|
||||
function emptyCart(): Cart {
|
||||
return { id: 'local', companyId: '', companyName: '', items: [] }
|
||||
}
|
||||
|
||||
function useLocalCart(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
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 {
|
||||
return cart.items
|
||||
.filter((item) => item.companyId === companyId)
|
||||
.reduce((sum, item) => sum + item.meterage, 0)
|
||||
}
|
||||
|
||||
export const useCartStore = defineStore('cart', () => {
|
||||
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) ?? carts.value[0] ?? null,
|
||||
)
|
||||
|
||||
const totalMeterage = computed(
|
||||
() => activeCart.value?.items.reduce((sum, i) => sum + i.meterage, 0) ?? 0,
|
||||
)
|
||||
|
||||
const itemCount = computed(() => activeCart.value?.items.length ?? 0)
|
||||
|
||||
const catalogStore = useCatalogStore()
|
||||
|
||||
const companyNamesLabel = computed(() => {
|
||||
if (!activeCart.value?.items.length) return ''
|
||||
return uniqueCompanyNames(activeCart.value.items)
|
||||
})
|
||||
|
||||
const companySummaries = computed(() => {
|
||||
const cart = activeCart.value
|
||||
if (!cart?.items.length) return [] as {
|
||||
companyId: string
|
||||
companyName: string
|
||||
meterage: number
|
||||
maxMeterage: number
|
||||
remaining: number
|
||||
}[]
|
||||
|
||||
const byCompany = new Map<string, { companyName: string; meterage: number }>()
|
||||
for (const item of cart.items) {
|
||||
const current = byCompany.get(item.companyId)
|
||||
if (current) {
|
||||
current.meterage += item.meterage
|
||||
} else {
|
||||
byCompany.set(item.companyId, {
|
||||
companyName: item.companyName || 'شرکت',
|
||||
meterage: item.meterage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return [...byCompany.entries()].map(([companyId, info]) => {
|
||||
const max =
|
||||
catalogStore.getCompany(companyId)?.maxMeteragePerInvoice ?? DEFAULT_MAX_METERAGE
|
||||
return {
|
||||
companyId,
|
||||
companyName: info.companyName,
|
||||
meterage: info.meterage,
|
||||
maxMeterage: max,
|
||||
remaining: Math.max(0, max - info.meterage),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/** سقف نمایشی: مجموع سقف شرکتهای داخل سبد (هر شرکت جداگانه محدود میشود). */
|
||||
const maxMeterage = computed(() => {
|
||||
if (!companySummaries.value.length) {
|
||||
const selected = selectedCompanyId.value
|
||||
? catalogStore.getCompany(selectedCompanyId.value)
|
||||
: null
|
||||
return selected?.maxMeteragePerInvoice ?? DEFAULT_MAX_METERAGE
|
||||
}
|
||||
return companySummaries.value.reduce((sum, row) => sum + row.maxMeterage, 0)
|
||||
})
|
||||
|
||||
const remainingCapacity = computed(() =>
|
||||
companySummaries.value.length
|
||||
? companySummaries.value.reduce((sum, row) => sum + row.remaining, 0)
|
||||
: Math.max(0, maxMeterage.value - totalMeterage.value),
|
||||
)
|
||||
|
||||
/** وقتی حداقل یک شرکت به سقف خودش رسیده باشد. */
|
||||
const isFull = computed(() =>
|
||||
companySummaries.value.some((row) => row.meterage >= row.maxMeterage),
|
||||
)
|
||||
|
||||
function applyServerCart(serverCart: ApiCart) {
|
||||
const cart: Cart = {
|
||||
id: serverCart.id,
|
||||
companyId: serverCart.companyId || '',
|
||||
companyName: serverCart.companyName || uniqueCompanyNames(serverCart.items),
|
||||
items: serverCart.items ?? [],
|
||||
notes: serverCart.notes,
|
||||
}
|
||||
carts.value = [cart]
|
||||
activeCartId.value = cart.id
|
||||
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')
|
||||
applyServerCart(serverCart)
|
||||
} catch {
|
||||
resetLocal()
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetLocal() {
|
||||
carts.value = [emptyCart()]
|
||||
activeCartId.value = carts.value[0]!.id
|
||||
selectedCompanyId.value = null
|
||||
}
|
||||
|
||||
function selectCompany(company: Company): void {
|
||||
selectedCompanyId.value = company.id
|
||||
if (activeCart.value && !activeCart.value.items.length) {
|
||||
activeCart.value.companyId = company.id
|
||||
activeCart.value.companyName = company.name
|
||||
}
|
||||
}
|
||||
|
||||
function initCartForCompany(company: Company): Cart {
|
||||
selectCompany(company)
|
||||
return activeCart.value ?? emptyCart()
|
||||
}
|
||||
|
||||
function getOrCreateCart(company?: Company): Cart {
|
||||
if (company) selectCompany(company)
|
||||
return activeCart.value ?? emptyCart()
|
||||
}
|
||||
|
||||
async function createNewCartForSameCompany(): Promise<Cart | null> {
|
||||
if (useLocalCart()) {
|
||||
resetLocal()
|
||||
return activeCart.value
|
||||
}
|
||||
try {
|
||||
const serverCart = await apiRequest<ApiCart>('/carts', { method: 'DELETE' })
|
||||
applyServerCart(serverCart)
|
||||
return activeCart.value
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
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: items[0]?.companyId || company.id,
|
||||
companyName: uniqueCompanyNames(items) || company.name,
|
||||
items,
|
||||
notes: cart.notes,
|
||||
})
|
||||
selectedCompanyId.value = company.id
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
async function addItem(
|
||||
product: Product,
|
||||
catalogCode: string,
|
||||
meterage: number,
|
||||
): Promise<{ success: boolean; message?: string }> {
|
||||
const company = catalogStore.getCompany(product.companyId)
|
||||
if (!company) {
|
||||
return { success: false, message: 'شرکت محصول یافت نشد.' }
|
||||
}
|
||||
|
||||
const cart = getOrCreateCart(company)
|
||||
const companyMax = company.maxMeteragePerInvoice
|
||||
const currentCompanyMeterage = companyMeterage(cart, company.id)
|
||||
const existing = cart.items.find((i) => i.productId === product.id)
|
||||
const previousMeterage = existing?.meterage ?? 0
|
||||
|
||||
const validation = validateMeterage(
|
||||
meterage,
|
||||
product,
|
||||
currentCompanyMeterage,
|
||||
companyMax,
|
||||
previousMeterage,
|
||||
)
|
||||
if (!validation.valid) {
|
||||
return { success: false, message: validation.message }
|
||||
}
|
||||
|
||||
if (useLocalCart()) {
|
||||
return addItemLocal(product, catalogCode, meterage, company)
|
||||
}
|
||||
|
||||
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 : 'افزودن به سبد ناموفق بود.',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateItemMeterage(
|
||||
itemId: string,
|
||||
meterage: number,
|
||||
product: Product,
|
||||
): 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 = catalogStore.getCompany(item.companyId || product.companyId)
|
||||
const companyMax = company?.maxMeteragePerInvoice ?? DEFAULT_MAX_METERAGE
|
||||
const currentCompanyMeterage = companyMeterage(activeCart.value, item.companyId)
|
||||
|
||||
const validation = validateMeterage(
|
||||
meterage,
|
||||
product,
|
||||
currentCompanyMeterage,
|
||||
companyMax,
|
||||
item.meterage,
|
||||
)
|
||||
if (!validation.valid) {
|
||||
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',
|
||||
body: { meterage },
|
||||
})
|
||||
applyServerCart(serverCart)
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
message: e instanceof ApiError ? e.message : 'بهروزرسانی متراژ ناموفق بود.',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
})
|
||||
applyServerCart(serverCart)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
body: { notes },
|
||||
})
|
||||
applyServerCart(serverCart)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function clearActiveCart() {
|
||||
if (useLocalCart()) {
|
||||
resetLocal()
|
||||
return
|
||||
}
|
||||
try {
|
||||
const serverCart = await apiRequest<ApiCart>('/carts', { method: 'DELETE' })
|
||||
applyServerCart(serverCart)
|
||||
} catch {
|
||||
resetLocal()
|
||||
}
|
||||
}
|
||||
|
||||
function ensureActiveCartForCartPage() {
|
||||
// server keeps a single cart
|
||||
}
|
||||
|
||||
return {
|
||||
carts,
|
||||
activeCartId,
|
||||
selectedCompanyId,
|
||||
syncing,
|
||||
activeCart,
|
||||
totalMeterage,
|
||||
itemCount,
|
||||
companyNamesLabel,
|
||||
companySummaries,
|
||||
maxMeterage,
|
||||
remainingCapacity,
|
||||
isFull,
|
||||
initCartForCompany,
|
||||
selectCompany,
|
||||
createNewCartForSameCompany,
|
||||
addItem,
|
||||
updateItemMeterage,
|
||||
removeItem,
|
||||
setNotes,
|
||||
clearActiveCart,
|
||||
ensureActiveCartForCartPage,
|
||||
hydrate,
|
||||
resetLocal,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,129 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { apiRequest } from '@/services/http'
|
||||
import type { BankAccount, Catalog, Company, Product } from '@/types'
|
||||
|
||||
export const useCatalogStore = defineStore('catalog', () => {
|
||||
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 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 = 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].sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
}
|
||||
|
||||
function fetchProducts(catalogId: string, colorFilter?: string): Product[] {
|
||||
let list = products.value.filter((p) => p.catalogId === catalogId && p.isActive)
|
||||
if (colorFilter && colorFilter !== 'all') {
|
||||
list = list.filter((p) => p.color.includes(colorFilter))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
function getCatalog(id: string) {
|
||||
return catalogs.value.find((c) => c.id === id)
|
||||
}
|
||||
|
||||
function getProduct(id: string) {
|
||||
return products.value.find((p) => p.id === id)
|
||||
}
|
||||
|
||||
function getUniqueColors(catalogId: string): string[] {
|
||||
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,
|
||||
fetchProducts,
|
||||
getCatalog,
|
||||
getProduct,
|
||||
getUniqueColors,
|
||||
getBankAccountsByCompany,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,206 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { apiRequest } from '@/services/http'
|
||||
|
||||
const STORAGE_KEY = 'ft_catalog_requests'
|
||||
|
||||
export type CatalogRequestItem = {
|
||||
productId: string
|
||||
productName: string
|
||||
productCode: string
|
||||
quantity: number
|
||||
}
|
||||
|
||||
/** UI statuses (legacy) + server statuses mapped in */
|
||||
export type CatalogRequestStatus =
|
||||
| 'submitted'
|
||||
| 'reviewing'
|
||||
| 'preparing'
|
||||
| 'shipped'
|
||||
| 'delivered'
|
||||
| 'new'
|
||||
| 'approved'
|
||||
| 'ready_to_ship'
|
||||
| 'returned'
|
||||
| 'cancelled'
|
||||
|
||||
export type CatalogRequest = {
|
||||
id: string
|
||||
createdAt: string
|
||||
status: CatalogRequestStatus
|
||||
note?: string
|
||||
items: CatalogRequestItem[]
|
||||
}
|
||||
|
||||
const SERVER_TO_UI: Record<string, CatalogRequestStatus> = {
|
||||
new: 'submitted',
|
||||
approved: 'reviewing',
|
||||
ready_to_ship: 'preparing',
|
||||
delivered: 'delivered',
|
||||
returned: 'delivered',
|
||||
cancelled: 'submitted',
|
||||
}
|
||||
|
||||
function readAll(): CatalogRequest[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return []
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
return Array.isArray(parsed) ? (parsed as CatalogRequest[]) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function writeAll(list: CatalogRequest[]) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(list))
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
submitted: 'ثبتشده',
|
||||
reviewing: 'در حال بررسی',
|
||||
preparing: 'آمادهسازی',
|
||||
shipped: 'ارسالشده',
|
||||
delivered: 'تحویلشده',
|
||||
new: 'درخواست جدید',
|
||||
approved: 'تأیید شده',
|
||||
ready_to_ship: 'آماده ارسال',
|
||||
returned: 'بازگردانده شده',
|
||||
cancelled: 'لغو شده',
|
||||
}
|
||||
|
||||
function mapServerRow(row: {
|
||||
id: string
|
||||
createdAt: string
|
||||
status: string
|
||||
note?: string
|
||||
items: Array<{
|
||||
productId: string
|
||||
productName: string
|
||||
productCode: string
|
||||
quantity: number
|
||||
}>
|
||||
}): CatalogRequest {
|
||||
return {
|
||||
id: row.id,
|
||||
createdAt: row.createdAt,
|
||||
status: SERVER_TO_UI[row.status] ?? (row.status as CatalogRequestStatus),
|
||||
note: row.note,
|
||||
items: row.items.map((i) => ({
|
||||
productId: i.productId,
|
||||
productName: i.productName,
|
||||
productCode: i.productCode,
|
||||
quantity: i.quantity,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export const useCatalogRequestsStore = defineStore('catalogRequests', () => {
|
||||
const requests = ref<CatalogRequest[]>(readAll())
|
||||
const useApi = ref(true)
|
||||
|
||||
const sorted = computed(() =>
|
||||
[...requests.value].sort((a, b) => b.createdAt.localeCompare(a.createdAt)),
|
||||
)
|
||||
|
||||
function statusLabel(status: CatalogRequest['status']) {
|
||||
return STATUS_LABELS[status] ?? status
|
||||
}
|
||||
|
||||
async function hydrate() {
|
||||
if (!useApi.value) {
|
||||
requests.value = readAll()
|
||||
return
|
||||
}
|
||||
try {
|
||||
const rows = await apiRequest<
|
||||
Array<{
|
||||
id: string
|
||||
createdAt: string
|
||||
status: string
|
||||
note?: string
|
||||
items: Array<{
|
||||
productId: string
|
||||
productName: string
|
||||
productCode: string
|
||||
quantity: number
|
||||
}>
|
||||
}>
|
||||
>('/catalog-requests')
|
||||
requests.value = rows.map(mapServerRow)
|
||||
writeAll(requests.value)
|
||||
} catch {
|
||||
useApi.value = false
|
||||
requests.value = readAll()
|
||||
}
|
||||
}
|
||||
|
||||
async function create(input: { items: CatalogRequestItem[]; note?: string }) {
|
||||
const items = input.items
|
||||
.filter((i) => i.productId && i.quantity > 0)
|
||||
.map((i) => ({
|
||||
...i,
|
||||
quantity: Math.max(1, Math.floor(i.quantity)),
|
||||
}))
|
||||
if (!items.length) {
|
||||
return { success: false as const, message: 'حداقل یک قلم با تعداد معتبر لازم است.' }
|
||||
}
|
||||
|
||||
if (useApi.value) {
|
||||
try {
|
||||
const row = await apiRequest<{
|
||||
id: string
|
||||
createdAt: string
|
||||
status: string
|
||||
note?: string
|
||||
items: Array<{
|
||||
productId: string
|
||||
productName: string
|
||||
productCode: string
|
||||
quantity: number
|
||||
}>
|
||||
}>('/catalog-requests', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
note: input.note?.trim() || undefined,
|
||||
items: items.map((i) => ({
|
||||
productId: i.productId,
|
||||
quantity: i.quantity,
|
||||
})),
|
||||
},
|
||||
})
|
||||
const mapped = mapServerRow(row)
|
||||
requests.value = [mapped, ...requests.value.filter((r) => r.id !== mapped.id)]
|
||||
writeAll(requests.value)
|
||||
return { success: true as const, request: mapped }
|
||||
} catch (err) {
|
||||
useApi.value = false
|
||||
// fall through to local
|
||||
}
|
||||
}
|
||||
|
||||
const row: CatalogRequest = {
|
||||
id: `cr-${Date.now()}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
status: 'submitted',
|
||||
note: input.note?.trim() || undefined,
|
||||
items,
|
||||
}
|
||||
requests.value = [row, ...requests.value]
|
||||
writeAll(requests.value)
|
||||
return { success: true as const, request: row }
|
||||
}
|
||||
|
||||
function getById(id: string) {
|
||||
return requests.value.find((r) => r.id === id) ?? null
|
||||
}
|
||||
|
||||
return {
|
||||
requests,
|
||||
sorted,
|
||||
statusLabel,
|
||||
create,
|
||||
getById,
|
||||
hydrate,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,199 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { CrmCustomer, CrmMeeting, CrmOrderSummary, CrmReminder } from '@/types/crm'
|
||||
import type { CatalogProductCard, ProductCategory, SeasonCategory } from '@/types/crm'
|
||||
import type { AgentProfileDto } from '@/services/agentApi'
|
||||
import {
|
||||
completeReminder as apiCompleteReminder,
|
||||
createAgentCustomer,
|
||||
createMeetingReport,
|
||||
createReminder as apiCreateReminder,
|
||||
fetchAgentCustomerDetail,
|
||||
fetchAgentCustomers,
|
||||
fetchAgentProfile,
|
||||
fetchBrowseProducts,
|
||||
fetchCatalogCategories,
|
||||
fetchReminderDetail,
|
||||
fetchReminders,
|
||||
} from '@/services/agentApi'
|
||||
import { apiRequest } from '@/services/http'
|
||||
|
||||
function mapProduct(p: {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
companyId?: string
|
||||
catalogId?: string
|
||||
catalogCode?: string
|
||||
color?: string
|
||||
minMeterage?: number
|
||||
orderMultiplier?: number
|
||||
basePrice?: number | null
|
||||
categoryId?: string
|
||||
usageCategory?: string
|
||||
season?: string
|
||||
widthCm?: number
|
||||
composition?: string
|
||||
weightGrams?: number
|
||||
weaveType?: string
|
||||
colors?: string[]
|
||||
tags?: string[]
|
||||
isNew?: boolean
|
||||
subtitle?: string
|
||||
rollMeters?: number
|
||||
imageUrl?: string
|
||||
imageUrls?: string[]
|
||||
}): CatalogProductCard {
|
||||
return {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
code: p.code,
|
||||
categoryId: p.categoryId || p.usageCategory || 'other',
|
||||
season: (p.season as CatalogProductCard['season']) || 'all_season',
|
||||
widthCm: p.widthCm ?? 150,
|
||||
composition: p.composition ?? '',
|
||||
weightGrams: p.weightGrams,
|
||||
weaveType: p.weaveType,
|
||||
colors: p.colors?.length ? p.colors : ['#6b7280'],
|
||||
tags: p.tags ?? [],
|
||||
isNew: !!p.isNew,
|
||||
imageUrl: p.imageUrl || p.imageUrls?.[0],
|
||||
subtitle: p.subtitle,
|
||||
rollMeters: p.rollMeters ?? 50,
|
||||
companyId: p.companyId,
|
||||
catalogId: p.catalogId,
|
||||
catalogCode: p.catalogCode,
|
||||
color: p.color,
|
||||
minMeterage: p.minMeterage ?? 30,
|
||||
orderMultiplier: p.orderMultiplier ?? 10,
|
||||
basePrice: p.basePrice ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export const useCrmStore = defineStore('crm', () => {
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const agentProfile = ref<AgentProfileDto | null>(null)
|
||||
const customers = ref<CrmCustomer[]>([])
|
||||
const reminders = ref<CrmReminder[]>([])
|
||||
const usageCategories = ref<ProductCategory[]>([])
|
||||
const seasonCategories = ref<SeasonCategory[]>([])
|
||||
const browseProducts = ref<CatalogProductCard[]>([])
|
||||
|
||||
async function loadCategories() {
|
||||
const data = await fetchCatalogCategories()
|
||||
usageCategories.value = data.usage
|
||||
seasonCategories.value = data.seasons as SeasonCategory[]
|
||||
return data
|
||||
}
|
||||
|
||||
async function loadBrowseProducts(query?: { categoryId?: string; season?: string }) {
|
||||
const list = await fetchBrowseProducts(query)
|
||||
browseProducts.value = list.map(mapProduct)
|
||||
return browseProducts.value
|
||||
}
|
||||
|
||||
async function loadProductById(id: string) {
|
||||
const p = await apiRequest<{
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
companyId?: string
|
||||
catalogId?: string
|
||||
catalogCode?: string
|
||||
color?: string
|
||||
minMeterage?: number
|
||||
orderMultiplier?: number
|
||||
basePrice?: number | null
|
||||
categoryId?: string
|
||||
usageCategory?: string
|
||||
season?: string
|
||||
widthCm?: number
|
||||
composition?: string
|
||||
weightGrams?: number
|
||||
weaveType?: string
|
||||
colors?: string[]
|
||||
tags?: string[]
|
||||
isNew?: boolean
|
||||
subtitle?: string
|
||||
rollMeters?: number
|
||||
imageUrl?: string
|
||||
imageUrls?: string[]
|
||||
}>(`/products/${id}`)
|
||||
return mapProduct(p)
|
||||
}
|
||||
|
||||
async function loadAgentProfile() {
|
||||
agentProfile.value = await fetchAgentProfile()
|
||||
return agentProfile.value
|
||||
}
|
||||
|
||||
async function loadCustomers(query?: { q?: string; status?: string }) {
|
||||
customers.value = (await fetchAgentCustomers(query)) as CrmCustomer[]
|
||||
return customers.value
|
||||
}
|
||||
|
||||
async function loadCustomerDetail(id: string) {
|
||||
return (await fetchAgentCustomerDetail(id)) as {
|
||||
customer: CrmCustomer
|
||||
orders: CrmOrderSummary[]
|
||||
meetings: CrmMeeting[]
|
||||
}
|
||||
}
|
||||
|
||||
async function submitMeeting(body: Parameters<typeof createMeetingReport>[0]) {
|
||||
return createMeetingReport(body)
|
||||
}
|
||||
|
||||
async function registerCustomer(body: Parameters<typeof createAgentCustomer>[0]) {
|
||||
return createAgentCustomer(body)
|
||||
}
|
||||
|
||||
async function loadReminders(filter?: string) {
|
||||
reminders.value = (await fetchReminders(filter)) as CrmReminder[]
|
||||
return reminders.value
|
||||
}
|
||||
|
||||
async function loadReminderDetail(id: string) {
|
||||
return fetchReminderDetail(id)
|
||||
}
|
||||
|
||||
async function markReminderComplete(id: string) {
|
||||
return apiCompleteReminder(id, true)
|
||||
}
|
||||
|
||||
async function createReminder(body: {
|
||||
customerId: string
|
||||
title: string
|
||||
date: string
|
||||
time: string
|
||||
description?: string
|
||||
note?: string
|
||||
interactionType?: string
|
||||
}) {
|
||||
return apiCreateReminder(body)
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
agentProfile,
|
||||
customers,
|
||||
reminders,
|
||||
usageCategories,
|
||||
seasonCategories,
|
||||
browseProducts,
|
||||
loadCategories,
|
||||
loadBrowseProducts,
|
||||
loadProductById,
|
||||
loadAgentProfile,
|
||||
loadCustomers,
|
||||
loadCustomerDetail,
|
||||
submitMeeting,
|
||||
registerCustomer,
|
||||
loadReminders,
|
||||
loadReminderDetail,
|
||||
markReminderComplete,
|
||||
createReminder,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,95 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { apiRequest, ApiError } from '@/services/http'
|
||||
|
||||
const LOCAL_KEY = 'ft_favorites'
|
||||
|
||||
function readLocal(): string[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(LOCAL_KEY)
|
||||
if (!raw) return []
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
return Array.isArray(parsed) ? parsed.filter((x) => typeof x === 'string') : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function writeLocal(ids: string[]) {
|
||||
localStorage.setItem(LOCAL_KEY, JSON.stringify(ids))
|
||||
}
|
||||
|
||||
export const useFavoritesStore = defineStore('favorites', () => {
|
||||
const ids = ref<string[]>(readLocal())
|
||||
const loaded = ref(false)
|
||||
const pending = ref<Record<string, boolean>>({})
|
||||
|
||||
const idSet = computed(() => new Set(ids.value))
|
||||
|
||||
function isFavorite(productId: string) {
|
||||
return idSet.value.has(productId)
|
||||
}
|
||||
|
||||
async function hydrate() {
|
||||
try {
|
||||
const list = await apiRequest<string[]>('/products/favorites/ids')
|
||||
ids.value = list
|
||||
writeLocal(list)
|
||||
} catch {
|
||||
ids.value = readLocal()
|
||||
} finally {
|
||||
loaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function toggle(productId: string) {
|
||||
if (!productId || pending.value[productId]) return
|
||||
pending.value = { ...pending.value, [productId]: true }
|
||||
|
||||
const wasFavorite = isFavorite(productId)
|
||||
ids.value = wasFavorite
|
||||
? ids.value.filter((id) => id !== productId)
|
||||
: [...ids.value, productId]
|
||||
writeLocal(ids.value)
|
||||
|
||||
try {
|
||||
const res = await apiRequest<{ productId: string; favorited: boolean }>(
|
||||
`/products/${productId}/favorite`,
|
||||
{ method: 'POST' },
|
||||
)
|
||||
ids.value = res.favorited
|
||||
? [...new Set([...ids.value.filter((id) => id !== productId), productId])]
|
||||
: ids.value.filter((id) => id !== productId)
|
||||
writeLocal(ids.value)
|
||||
} catch (e) {
|
||||
// اگر endpoint روی سرور نبود، حالت محلی حفظ میشود
|
||||
if (!(e instanceof ApiError && e.status === 404)) {
|
||||
ids.value = wasFavorite
|
||||
? [...new Set([...ids.value, productId])]
|
||||
: ids.value.filter((id) => id !== productId)
|
||||
writeLocal(ids.value)
|
||||
throw e
|
||||
}
|
||||
} finally {
|
||||
const next = { ...pending.value }
|
||||
delete next[productId]
|
||||
pending.value = next
|
||||
}
|
||||
}
|
||||
|
||||
function resetLocal() {
|
||||
ids.value = []
|
||||
writeLocal([])
|
||||
loaded.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
ids,
|
||||
loaded,
|
||||
pending,
|
||||
isFavorite,
|
||||
hydrate,
|
||||
toggle,
|
||||
resetLocal,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { apiRequest, getAccessToken } from '@/services/http'
|
||||
import type { AppNotification } from '@/types'
|
||||
|
||||
export const useNotificationStore = defineStore('notification', () => {
|
||||
const items = ref<AppNotification[]>([])
|
||||
const unreadCount = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
const hasUnread = computed(() => unreadCount.value > 0)
|
||||
|
||||
async function hydrate() {
|
||||
if (!getAccessToken()) {
|
||||
items.value = []
|
||||
unreadCount.value = 0
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
items.value = await apiRequest<AppNotification[]>('/notifications')
|
||||
unreadCount.value = items.value.filter((n) => !n.isRead).length
|
||||
} catch {
|
||||
items.value = []
|
||||
unreadCount.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** فقط شمارنده؛ برای زنگ هدر که نیازی به کل فهرست ندارد. */
|
||||
async function refreshUnreadCount() {
|
||||
if (!getAccessToken()) {
|
||||
unreadCount.value = 0
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await apiRequest<{ count: number }>('/notifications/unread-count')
|
||||
unreadCount.value = res.count
|
||||
} catch {
|
||||
/* شمارنده حیاتی نیست */
|
||||
}
|
||||
}
|
||||
|
||||
async function markRead(id: string) {
|
||||
const item = items.value.find((n) => n.id === id)
|
||||
if (!item || item.isRead) return
|
||||
item.isRead = true
|
||||
unreadCount.value = Math.max(0, unreadCount.value - 1)
|
||||
try {
|
||||
await apiRequest(`/notifications/${id}/read`, { method: 'PATCH' })
|
||||
} catch {
|
||||
await hydrate()
|
||||
}
|
||||
}
|
||||
|
||||
async function markAllRead() {
|
||||
if (!unreadCount.value) return
|
||||
items.value = items.value.map((n) => ({ ...n, isRead: true }))
|
||||
unreadCount.value = 0
|
||||
try {
|
||||
await apiRequest('/notifications/read-all', { method: 'PATCH' })
|
||||
} catch {
|
||||
await hydrate()
|
||||
}
|
||||
}
|
||||
|
||||
function resetLocal() {
|
||||
items.value = []
|
||||
unreadCount.value = 0
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
unreadCount,
|
||||
loading,
|
||||
hasUnread,
|
||||
hydrate,
|
||||
refreshUnreadCount,
|
||||
markRead,
|
||||
markAllRead,
|
||||
resetLocal,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,292 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type {
|
||||
Cart,
|
||||
CheckData,
|
||||
CashPaymentReceipt,
|
||||
Order,
|
||||
SettlementType,
|
||||
CheckRegistrationMode,
|
||||
} from '@/types'
|
||||
import { generateId } from '@/utils/format'
|
||||
import { calculateCheckRass } from '@/utils/checkRass'
|
||||
import { apiRequest, ApiError, getAccessToken } from '@/services/http'
|
||||
import { useCatalogStore } from './catalog'
|
||||
|
||||
export const useOrderStore = defineStore('order', () => {
|
||||
const orders = ref<Order[]>([])
|
||||
const draftChecks = ref<CheckData[]>([])
|
||||
const draftSettlementType = ref<SettlementType | null>(null)
|
||||
const draftCheckMode = ref<CheckRegistrationMode | null>(null)
|
||||
const draftOrderId = ref<string | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const userOrders = computed(
|
||||
() => (_userId: string) =>
|
||||
[...orders.value].sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
),
|
||||
)
|
||||
|
||||
function getOrderById(id: string): Order | undefined {
|
||||
return orders.value.find((o) => o.id === id)
|
||||
}
|
||||
|
||||
/** شروع مرحله ثبت چک روی سفارشی که فاکتورش صادر شده است. */
|
||||
function startCheckDraft(orderId: string, mode?: CheckRegistrationMode) {
|
||||
draftOrderId.value = orderId
|
||||
draftSettlementType.value = 'check'
|
||||
draftChecks.value = []
|
||||
draftCheckMode.value = mode ?? null
|
||||
}
|
||||
|
||||
/** برای اصلاح چک، چکهای ثبتشده سفارش به فرم برگردانده میشوند. */
|
||||
function loadChecksIntoDraft(order: Order) {
|
||||
draftOrderId.value = order.id
|
||||
draftSettlementType.value = 'check'
|
||||
draftCheckMode.value = order.checkMode ?? 'full'
|
||||
draftChecks.value = (order.checks ?? []).map((c) => ({ ...c }))
|
||||
}
|
||||
|
||||
function clearCheckDraft() {
|
||||
draftOrderId.value = null
|
||||
draftChecks.value = []
|
||||
draftCheckMode.value = null
|
||||
draftSettlementType.value = null
|
||||
}
|
||||
|
||||
function setCheckMode(mode: CheckRegistrationMode) {
|
||||
draftCheckMode.value = mode
|
||||
}
|
||||
|
||||
function addDraftCheck(check: Omit<CheckData, 'id'>) {
|
||||
draftChecks.value.push({ ...check, id: generateId() })
|
||||
}
|
||||
|
||||
function updateDraftCheck(id: string, data: Partial<CheckData>) {
|
||||
const index = draftChecks.value.findIndex((c) => c.id === id)
|
||||
if (index >= 0) {
|
||||
draftChecks.value[index] = { ...draftChecks.value[index]!, ...data }
|
||||
}
|
||||
}
|
||||
|
||||
function removeDraftCheck(id: string) {
|
||||
draftChecks.value = draftChecks.value.filter((c) => c.id !== id)
|
||||
}
|
||||
|
||||
function getDraftRass(baseDate?: Date) {
|
||||
return calculateCheckRass(draftChecks.value, baseDate ?? new Date())
|
||||
}
|
||||
|
||||
async function hydrate() {
|
||||
if (!getAccessToken()) {
|
||||
orders.value = []
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
orders.value = await apiRequest<Order[]>('/orders')
|
||||
} catch {
|
||||
orders.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetLocal() {
|
||||
orders.value = []
|
||||
clearCheckDraft()
|
||||
}
|
||||
|
||||
function upsertOrder(order: Order) {
|
||||
const idx = orders.value.findIndex((o) => o.id === order.id)
|
||||
if (idx >= 0) orders.value[idx] = order
|
||||
else orders.value.unshift(order)
|
||||
}
|
||||
|
||||
async function fetchOrder(id: string): Promise<Order | null> {
|
||||
try {
|
||||
const order = await apiRequest<Order>(`/orders/${id}`)
|
||||
upsertOrder(order)
|
||||
return order
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* سفارش بدون روش تسویه ثبت میشود؛ مشتری بعد از قیمتگذاری مدیریت
|
||||
* روش تسویه را انتخاب میکند.
|
||||
* اقلام همه شرکتها در یک فاکتور واحد ثبت میشوند.
|
||||
*/
|
||||
async function submitOrder(cart: Cart): Promise<Order> {
|
||||
if (!cart.items.length) {
|
||||
throw new Error('سبد خرید خالی است.')
|
||||
}
|
||||
if (cart.id === 'local' || cart.items.some((item) => item.id.startsWith('local-item-'))) {
|
||||
throw new Error('سبد خرید با سرور همگام نیست. لطفاً دوباره محصولات را به سبد اضافه کنید.')
|
||||
}
|
||||
|
||||
const companyIds = [
|
||||
...new Set(cart.items.map((item) => item.companyId).filter(Boolean)),
|
||||
]
|
||||
if (!companyIds.length) {
|
||||
throw new Error('شرکت سبد مشخص نیست. سبد را خالی کنید و دوباره اضافه کنید.')
|
||||
}
|
||||
|
||||
try {
|
||||
const order = await apiRequest<Order>('/orders', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
companyId: companyIds[0],
|
||||
notes: cart.notes,
|
||||
items: cart.items.map((item) => ({
|
||||
productId: item.productId,
|
||||
meterage: Number(item.meterage),
|
||||
notes: item.notes,
|
||||
})),
|
||||
},
|
||||
})
|
||||
orders.value.unshift(order)
|
||||
clearCheckDraft()
|
||||
return order
|
||||
} catch (e) {
|
||||
throw new Error(e instanceof ApiError ? e.message : 'ثبت سفارش ناموفق بود.')
|
||||
}
|
||||
}
|
||||
|
||||
/** انتخاب روش تسویه پس از صدور فاکتور. */
|
||||
async function chooseSettlement(
|
||||
orderId: string,
|
||||
settlementType: SettlementType,
|
||||
checkMode?: CheckRegistrationMode,
|
||||
): Promise<Order> {
|
||||
try {
|
||||
const order = await apiRequest<Order>(`/orders/${orderId}/settlement`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
settlementType,
|
||||
checkMode: settlementType === 'check' ? (checkMode ?? 'full') : undefined,
|
||||
},
|
||||
})
|
||||
upsertOrder(order)
|
||||
return order
|
||||
} catch (e) {
|
||||
throw new Error(e instanceof ApiError ? e.message : 'ثبت روش تسویه ناموفق بود.')
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCashReceipt(
|
||||
orderId: string,
|
||||
receipt: Omit<CashPaymentReceipt, 'id' | 'submittedAt'>,
|
||||
) {
|
||||
try {
|
||||
await apiRequest(`/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 order = await fetchOrder(orderId)
|
||||
if (!order) {
|
||||
throw new Error('رسید ثبت شد ولی دریافت سفارش ناموفق بود.')
|
||||
}
|
||||
return order
|
||||
} catch (e) {
|
||||
throw new Error(e instanceof ApiError ? e.message : 'ثبت رسید ناموفق بود.')
|
||||
}
|
||||
}
|
||||
|
||||
async function updateOrderChecks(
|
||||
orderId: string,
|
||||
checks: CheckData[],
|
||||
checkMode?: CheckRegistrationMode,
|
||||
): Promise<Order> {
|
||||
try {
|
||||
const order = await apiRequest<Order>(`/orders/${orderId}/checks`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
checkMode,
|
||||
checks: checks.map((c) => ({
|
||||
amount: c.amount,
|
||||
dueDate: c.dueDate,
|
||||
sayadId: c.sayadId,
|
||||
checkNumber: c.checkNumber,
|
||||
ownerNationalId: c.ownerNationalId,
|
||||
ownerFirstName: c.ownerFirstName,
|
||||
ownerLastName: c.ownerLastName,
|
||||
bankName: c.bankName,
|
||||
branchName: c.branchName,
|
||||
frontImage: c.frontImage,
|
||||
backImage: c.backImage,
|
||||
notes: c.notes,
|
||||
mode: c.mode,
|
||||
})),
|
||||
},
|
||||
})
|
||||
upsertOrder(order)
|
||||
clearCheckDraft()
|
||||
return order
|
||||
} catch (e) {
|
||||
throw new Error(e instanceof ApiError ? e.message : 'ثبت چکها ناموفق بود.')
|
||||
}
|
||||
}
|
||||
|
||||
async function getBankAccountsForOrder(order: Order) {
|
||||
const companyIds = [
|
||||
...new Set(
|
||||
[
|
||||
order.companyId,
|
||||
...order.items.map((item) => item.companyId).filter(Boolean),
|
||||
].filter(Boolean) as string[],
|
||||
),
|
||||
]
|
||||
const catalog = useCatalogStore()
|
||||
const lists = await Promise.all(companyIds.map((id) => catalog.loadBankAccounts(id)))
|
||||
const byId = new Map<string, (typeof lists)[number][number]>()
|
||||
for (const list of lists) {
|
||||
for (const account of list) {
|
||||
byId.set(account.id, account)
|
||||
}
|
||||
}
|
||||
return [...byId.values()]
|
||||
}
|
||||
|
||||
async function getBankAccountsForCompany(companyId: string) {
|
||||
return useCatalogStore().loadBankAccounts(companyId)
|
||||
}
|
||||
|
||||
return {
|
||||
orders,
|
||||
draftChecks,
|
||||
draftSettlementType,
|
||||
draftCheckMode,
|
||||
draftOrderId,
|
||||
loading,
|
||||
userOrders,
|
||||
getOrderById,
|
||||
startCheckDraft,
|
||||
loadChecksIntoDraft,
|
||||
clearCheckDraft,
|
||||
setCheckMode,
|
||||
addDraftCheck,
|
||||
updateDraftCheck,
|
||||
removeDraftCheck,
|
||||
getDraftRass,
|
||||
submitOrder,
|
||||
chooseSettlement,
|
||||
submitCashReceipt,
|
||||
updateOrderChecks,
|
||||
hydrate,
|
||||
resetLocal,
|
||||
fetchOrder,
|
||||
getBankAccountsForOrder,
|
||||
getBankAccountsForCompany,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
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
|
||||
}
|
||||
|
||||
function cyclePreference() {
|
||||
const order: ThemePreference[] = ['light', 'dark', 'system']
|
||||
const index = order.indexOf(preference.value)
|
||||
preference.value = order[(index + 1) % order.length]!
|
||||
}
|
||||
|
||||
/** Explicit light ↔ dark toggle (skips system). */
|
||||
function toggleLightDark() {
|
||||
preference.value = isDark.value ? 'light' : 'dark'
|
||||
}
|
||||
|
||||
return {
|
||||
preference,
|
||||
resolved,
|
||||
isDark,
|
||||
setPreference,
|
||||
cyclePreference,
|
||||
toggleLightDark,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user