Move settlement after invoice and wire real notification inbox.

Submit orders without choosing payment first, route cash/check flows under /orders/:id, show status-based next actions, and connect the header bell to unread notifications.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-09 12:00:18 +03:30
co-authored by Cursor
parent 6ee3c6f1f8
commit 7acd225fad
23 changed files with 805 additions and 333 deletions
+3
View File
@@ -17,6 +17,7 @@ import {
import { useCartStore } from './cart'
import { useOrderStore } from './order'
import { useCatalogStore } from './catalog'
import { useNotificationStore } from './notification'
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(getCurrentUser())
@@ -82,6 +83,7 @@ export const useAuthStore = defineStore('auth', () => {
useCatalogStore().loadCompanies(),
useCartStore().hydrate(),
useOrderStore().hydrate(),
useNotificationStore().hydrate(),
])
} else {
error.value = result.message
@@ -95,6 +97,7 @@ export const useAuthStore = defineStore('auth', () => {
clearPendingRegister()
useCartStore().resetLocal()
useOrderStore().resetLocal()
useNotificationStore().resetLocal()
}
async function updateProfile(data: Partial<Pick<User, 'firstName' | 'lastName' | 'nationalId'>>) {
+84
View File
@@ -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,
}
})
+70 -49
View File
@@ -18,7 +18,7 @@ export const useOrderStore = defineStore('order', () => {
const draftChecks = ref<CheckData[]>([])
const draftSettlementType = ref<SettlementType | null>(null)
const draftCheckMode = ref<CheckRegistrationMode | null>(null)
const checkoutCartId = ref<string | null>(null)
const draftOrderId = ref<string | null>(null)
const loading = ref(false)
const userOrders = computed(
@@ -32,11 +32,27 @@ export const useOrderStore = defineStore('order', () => {
return orders.value.find((o) => o.id === id)
}
function startCheckout(cart: Cart, settlementType: SettlementType) {
checkoutCartId.value = cart.id
draftSettlementType.value = settlementType
/** شروع مرحله ثبت چک روی سفارشی که فاکتورش صادر شده است. */
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) {
@@ -79,31 +95,30 @@ export const useOrderStore = defineStore('order', () => {
function resetLocal() {
orders.value = []
draftChecks.value = []
draftSettlementType.value = null
draftCheckMode.value = null
checkoutCartId.value = null
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}`)
const idx = orders.value.findIndex((o) => o.id === id)
if (idx >= 0) orders.value[idx] = order
else orders.value.unshift(order)
upsertOrder(order)
return order
} catch {
return null
}
}
async function submitOrder(
_userId: string,
cart: Cart,
settlementType: SettlementType,
checks?: CheckData[],
checkMode?: CheckRegistrationMode,
): Promise<Order> {
/**
* سفارش بدون روش تسویه ثبت می‌شود؛ مشتری بعد از قیمت‌گذاری مدیریت
* روش تسویه را انتخاب می‌کند.
*/
async function submitOrder(cart: Cart): Promise<Order> {
const companyId = cart.companyId || cart.items[0]?.companyId || ''
if (!companyId) {
throw new Error('شرکت سبد مشخص نیست.')
@@ -120,32 +135,12 @@ export const useOrderStore = defineStore('order', () => {
const body = {
companyId,
settlementType,
checkMode: settlementType === 'check' ? (checkMode ?? 'full') : undefined,
notes: cart.notes,
items: itemsForOrder.map((item) => ({
productId: item.productId,
meterage: item.meterage,
notes: item.notes,
})),
checks:
settlementType === 'check' && 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,
}))
: undefined,
}
try {
@@ -154,16 +149,34 @@ export const useOrderStore = defineStore('order', () => {
body,
})
orders.value.unshift(order)
draftChecks.value = []
draftSettlementType.value = null
draftCheckMode.value = null
checkoutCartId.value = null
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'>,
@@ -191,11 +204,16 @@ export const useOrderStore = defineStore('order', () => {
}
}
async function updateOrderChecks(orderId: string, checks: CheckData[]) {
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,
@@ -213,11 +231,11 @@ export const useOrderStore = defineStore('order', () => {
})),
},
})
const idx = orders.value.findIndex((o) => o.id === orderId)
if (idx >= 0) orders.value[idx] = order
upsertOrder(order)
clearCheckDraft()
return order
} catch {
return null
} catch (e) {
throw new Error(e instanceof ApiError ? e.message : 'ثبت چک‌ها ناموفق بود.')
}
}
@@ -230,17 +248,20 @@ export const useOrderStore = defineStore('order', () => {
draftChecks,
draftSettlementType,
draftCheckMode,
checkoutCartId,
draftOrderId,
loading,
userOrders,
getOrderById,
startCheckout,
startCheckDraft,
loadChecksIntoDraft,
clearCheckDraft,
setCheckMode,
addDraftCheck,
updateDraftCheck,
removeDraftCheck,
getDraftRass,
submitOrder,
chooseSettlement,
submitCashReceipt,
updateOrderChecks,
hydrate,