Initial commit: fabric-trade-user Vue 3 app with mobile and desktop layouts
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { User } from '@/types'
|
||||
import {
|
||||
getCurrentUser,
|
||||
logout as authLogout,
|
||||
sendOtp,
|
||||
verifyOtp,
|
||||
updateProfile as authUpdateProfile,
|
||||
} from '@/services/authService'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<User | null>(getCurrentUser())
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const isAuthenticated = computed(() => !!user.value)
|
||||
const fullName = computed(() =>
|
||||
user.value ? `${user.value.firstName} ${user.value.lastName}` : '',
|
||||
)
|
||||
|
||||
function clearError() {
|
||||
error.value = 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
|
||||
}
|
||||
|
||||
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
|
||||
} else {
|
||||
error.value = result.message
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function logout() {
|
||||
authLogout()
|
||||
user.value = null
|
||||
}
|
||||
|
||||
function updateProfile(data: Partial<Pick<User, 'firstName' | 'lastName' | 'nationalId'>>) {
|
||||
if (!user.value) return null
|
||||
const updated = authUpdateProfile(user.value.id, data)
|
||||
if (updated) user.value = updated
|
||||
return updated
|
||||
}
|
||||
|
||||
function hydrate() {
|
||||
user.value = getCurrentUser()
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
loading,
|
||||
error,
|
||||
isAuthenticated,
|
||||
fullName,
|
||||
clearError,
|
||||
requestOtp,
|
||||
confirmOtp,
|
||||
logout,
|
||||
updateProfile,
|
||||
hydrate,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,260 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Cart, CartItem, Company, Product } from '@/types'
|
||||
import { generateId } from '@/utils/format'
|
||||
import { validateMeterage } from '@/utils/validation'
|
||||
import { getItem, setItem, STORAGE_KEYS } from '@/services/storage'
|
||||
import { getCompanyById } from '@/data/mockData'
|
||||
|
||||
function loadCarts(): Cart[] {
|
||||
return getItem<Cart[]>(STORAGE_KEYS.carts, [])
|
||||
}
|
||||
|
||||
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 findOpenCartForCompany(carts: Cart[], company: Company): Cart | undefined {
|
||||
return carts.find(
|
||||
(cart) =>
|
||||
cart.companyId === company.id &&
|
||||
getCartTotalMeterage(cart) < company.maxMeteragePerInvoice,
|
||||
)
|
||||
}
|
||||
|
||||
export const useCartStore = defineStore('cart', () => {
|
||||
const carts = ref<Cart[]>(loadCarts())
|
||||
const activeCartId = ref<string | null>(loadActiveCartId())
|
||||
const selectedCompanyId = ref<string | null>(null)
|
||||
|
||||
const activeCart = computed(() => carts.value.find((c) => c.id === activeCartId.value) ?? null)
|
||||
|
||||
const totalMeterage = computed(
|
||||
() => activeCart.value?.items.reduce((sum, i) => sum + i.meterage, 0) ?? 0,
|
||||
)
|
||||
|
||||
const itemCount = computed(() => activeCart.value?.items.length ?? 0)
|
||||
|
||||
const maxMeterage = computed(() => {
|
||||
if (!activeCart.value) return 5000
|
||||
const company = getCompanyById(activeCart.value.companyId)
|
||||
return company?.maxMeteragePerInvoice ?? 5000
|
||||
})
|
||||
|
||||
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 initCartForCompany(company: Company): Cart {
|
||||
const existing = findOpenCartForCompany(carts.value, company)
|
||||
if (existing) {
|
||||
activeCartId.value = existing.id
|
||||
selectedCompanyId.value = company.id
|
||||
persist()
|
||||
return existing
|
||||
}
|
||||
|
||||
const cart: Cart = {
|
||||
id: generateId(),
|
||||
companyId: company.id,
|
||||
companyName: company.name,
|
||||
items: [],
|
||||
}
|
||||
carts.value.push(cart)
|
||||
activeCartId.value = cart.id
|
||||
selectedCompanyId.value = company.id
|
||||
persist()
|
||||
return cart
|
||||
}
|
||||
|
||||
function switchCompanyWarning(currentCompanyName: string, newCompanyName: string): string {
|
||||
return `سبد خرید فعلی مربوط به ${currentCompanyName} است. برای خرید از ${newCompanyName} باید سبد خرید جداگانهای ایجاد کنید.`
|
||||
}
|
||||
|
||||
function canSwitchToCompany(companyId: string): { allowed: boolean; message?: string } {
|
||||
if (!activeCart.value || activeCart.value.items.length === 0) {
|
||||
return { allowed: true }
|
||||
}
|
||||
if (activeCart.value.companyId === companyId) {
|
||||
return { allowed: true }
|
||||
}
|
||||
const newCompany = getCompanyById(companyId)
|
||||
return {
|
||||
allowed: false,
|
||||
message: switchCompanyWarning(activeCart.value.companyName, newCompany?.name ?? ''),
|
||||
}
|
||||
}
|
||||
|
||||
function createNewCartForSameCompany(): Cart | null {
|
||||
if (!activeCart.value) return null
|
||||
const company = getCompanyById(activeCart.value.companyId)
|
||||
if (!company) return null
|
||||
const cart: Cart = {
|
||||
id: generateId(),
|
||||
companyId: company.id,
|
||||
companyName: company.name,
|
||||
items: [],
|
||||
}
|
||||
carts.value.push(cart)
|
||||
activeCartId.value = cart.id
|
||||
persist()
|
||||
return cart
|
||||
}
|
||||
|
||||
function addItem(
|
||||
product: Product,
|
||||
catalogCode: string,
|
||||
meterage: number,
|
||||
): { success: boolean; message?: string } {
|
||||
if (!activeCart.value) {
|
||||
return { success: false, message: 'ابتدا شرکت را انتخاب کنید.' }
|
||||
}
|
||||
|
||||
if (activeCart.value.companyId !== product.companyId) {
|
||||
const currentCompany = activeCart.value.companyName
|
||||
const newCompany = getCompanyById(product.companyId)
|
||||
return {
|
||||
success: false,
|
||||
message: switchCompanyWarning(currentCompany, newCompany?.name ?? ''),
|
||||
}
|
||||
}
|
||||
|
||||
const validation = validateMeterage(meterage, product, totalMeterage.value, maxMeterage.value)
|
||||
if (!validation.valid) {
|
||||
return { success: false, message: validation.message }
|
||||
}
|
||||
|
||||
const existingIndex = activeCart.value.items.findIndex((i) => i.productId === product.id)
|
||||
if (existingIndex >= 0) {
|
||||
const existing = activeCart.value.items[existingIndex]!
|
||||
const revalidation = validateMeterage(
|
||||
meterage,
|
||||
product,
|
||||
totalMeterage.value,
|
||||
maxMeterage.value,
|
||||
existing.meterage,
|
||||
)
|
||||
if (!revalidation.valid) {
|
||||
return { success: false, message: revalidation.message }
|
||||
}
|
||||
existing.meterage = meterage
|
||||
} else {
|
||||
const item: CartItem = {
|
||||
id: generateId(),
|
||||
productId: product.id,
|
||||
catalogCode,
|
||||
productName: product.name,
|
||||
productCode: product.code,
|
||||
color: product.color,
|
||||
meterage,
|
||||
unitPrice: product.basePrice,
|
||||
}
|
||||
activeCart.value.items.push(item)
|
||||
}
|
||||
|
||||
persist()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
function updateItemMeterage(
|
||||
itemId: string,
|
||||
meterage: number,
|
||||
product: Product,
|
||||
): { 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 validation = validateMeterage(
|
||||
meterage,
|
||||
product,
|
||||
totalMeterage.value,
|
||||
maxMeterage.value,
|
||||
item.meterage,
|
||||
)
|
||||
if (!validation.valid) {
|
||||
return { success: false, message: validation.message }
|
||||
}
|
||||
|
||||
item.meterage = meterage
|
||||
persist()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
function removeItem(itemId: string) {
|
||||
if (!activeCart.value) return
|
||||
activeCart.value.items = activeCart.value.items.filter((i) => i.id !== itemId)
|
||||
persist()
|
||||
}
|
||||
|
||||
function setNotes(notes: string) {
|
||||
if (!activeCart.value) return
|
||||
activeCart.value.notes = notes
|
||||
persist()
|
||||
}
|
||||
|
||||
function clearActiveCart() {
|
||||
if (!activeCart.value) return
|
||||
activeCart.value.items = []
|
||||
activeCart.value.notes = undefined
|
||||
persist()
|
||||
}
|
||||
|
||||
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 = cartWithItems.companyId
|
||||
persist()
|
||||
}
|
||||
|
||||
function hydrate() {
|
||||
carts.value = loadCarts()
|
||||
activeCartId.value = loadActiveCartId()
|
||||
if (activeCart.value) {
|
||||
selectedCompanyId.value = activeCart.value.companyId
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
carts,
|
||||
activeCartId,
|
||||
selectedCompanyId,
|
||||
activeCart,
|
||||
totalMeterage,
|
||||
itemCount,
|
||||
maxMeterage,
|
||||
remainingCapacity,
|
||||
isFull,
|
||||
initCartForCompany,
|
||||
canSwitchToCompany,
|
||||
createNewCartForSameCompany,
|
||||
addItem,
|
||||
updateItemMeterage,
|
||||
removeItem,
|
||||
setNotes,
|
||||
clearActiveCart,
|
||||
ensureActiveCartForCartPage,
|
||||
hydrate,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
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'
|
||||
|
||||
export const useCatalogStore = defineStore('catalog', () => {
|
||||
const companies = ref<Company[]>(mockCompanies.filter((c) => c.isActive))
|
||||
const searchQuery = ref('')
|
||||
|
||||
function getActiveCompanies(): Company[] {
|
||||
return [...companies.value].sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
}
|
||||
|
||||
function getCompany(id: string): Company | undefined {
|
||||
return getCompanyById(id)
|
||||
}
|
||||
|
||||
function fetchCatalogs(companyId: string, query = ''): Catalog[] {
|
||||
let list = getCatalogsByCompany(companyId)
|
||||
if (query.trim()) {
|
||||
const q = query.trim()
|
||||
list = list.filter((c) => c.code.includes(q) || c.name.includes(q))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
function fetchProducts(catalogId: string, colorFilter?: string): Product[] {
|
||||
let list = getProductsByCatalog(catalogId)
|
||||
if (colorFilter && colorFilter !== 'all') {
|
||||
list = list.filter((p) => p.color.includes(colorFilter))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
function getCatalog(id: string) {
|
||||
return getCatalogById(id)
|
||||
}
|
||||
|
||||
function getProduct(id: string) {
|
||||
return getProductById(id)
|
||||
}
|
||||
|
||||
function getUniqueColors(catalogId: string): string[] {
|
||||
const products = getProductsByCatalog(catalogId)
|
||||
return [...new Set(products.map((p) => p.color))]
|
||||
}
|
||||
|
||||
return {
|
||||
companies,
|
||||
searchQuery,
|
||||
getActiveCompanies,
|
||||
getCompany,
|
||||
fetchCatalogs,
|
||||
fetchProducts,
|
||||
getCatalog,
|
||||
getProduct,
|
||||
getUniqueColors,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,248 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type {
|
||||
Cart,
|
||||
CheckData,
|
||||
CashPaymentReceipt,
|
||||
Order,
|
||||
OrderItem,
|
||||
OrderStatus,
|
||||
OrderStatusHistoryEntry,
|
||||
SettlementType,
|
||||
CheckRegistrationMode,
|
||||
} from '@/types'
|
||||
import { generateId, generateOrderNumber } 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()
|
||||
}
|
||||
|
||||
export const useOrderStore = defineStore('order', () => {
|
||||
const orders = ref<Order[]>(loadOrders())
|
||||
const draftChecks = ref<CheckData[]>([])
|
||||
const draftSettlementType = ref<SettlementType | null>(null)
|
||||
const draftCheckMode = ref<CheckRegistrationMode | null>(null)
|
||||
const checkoutCartId = ref<string | null>(null)
|
||||
|
||||
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()),
|
||||
)
|
||||
|
||||
function persist() {
|
||||
saveOrders(orders.value)
|
||||
}
|
||||
|
||||
function getOrderById(id: string): Order | undefined {
|
||||
return orders.value.find((o) => o.id === id)
|
||||
}
|
||||
|
||||
function startCheckout(cart: Cart, settlementType: SettlementType) {
|
||||
checkoutCartId.value = cart.id
|
||||
draftSettlementType.value = settlementType
|
||||
draftChecks.value = []
|
||||
draftCheckMode.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())
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
if (settlementType === 'cash') {
|
||||
cashInvoiceExpiresAt = computeCashExpiry(cart.companyId)
|
||||
status = 'awaiting_review'
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
draftChecks.value = []
|
||||
draftSettlementType.value = null
|
||||
draftCheckMode.value = null
|
||||
checkoutCartId.value = null
|
||||
|
||||
return order
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function hydrate() {
|
||||
orders.value = loadOrders()
|
||||
}
|
||||
|
||||
return {
|
||||
orders,
|
||||
draftChecks,
|
||||
draftSettlementType,
|
||||
draftCheckMode,
|
||||
checkoutCartId,
|
||||
userOrders,
|
||||
persist,
|
||||
getOrderById,
|
||||
startCheckout,
|
||||
setCheckMode,
|
||||
addDraftCheck,
|
||||
updateDraftCheck,
|
||||
removeDraftCheck,
|
||||
getDraftRass,
|
||||
submitOrder,
|
||||
submitCashReceipt,
|
||||
updateOrderChecks,
|
||||
hydrate,
|
||||
getBankAccountsForCompany: getBankAccountsByCompany,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user