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:
2026-08-03 09:57:24 +02:00
co-authored by Cursor
commit fd3e5e5126
142 changed files with 19798 additions and 0 deletions
+80
View File
@@ -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,
}
})
+260
View File
@@ -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,
}
})
+66
View File
@@ -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,
}
})
+248
View File
@@ -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,
}
})