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:
+13
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { RouterView, useRouter } from 'vue-router'
|
||||
import { useIntervalFn } from '@vueuse/core'
|
||||
import { clamp } from 'lodash-es'
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog.vue'
|
||||
@@ -8,11 +9,13 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import { useCatalogStore } from '@/stores/catalog'
|
||||
import { useNotificationStore } from '@/stores/notification'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const cartStore = useCartStore()
|
||||
const orderStore = useOrderStore()
|
||||
const catalogStore = useCatalogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const router = useRouter()
|
||||
|
||||
const transitionName = ref('page-fade')
|
||||
@@ -40,9 +43,19 @@ onMounted(async () => {
|
||||
catalogStore.loadCompanies(),
|
||||
cartStore.hydrate(),
|
||||
orderStore.hydrate(),
|
||||
notificationStore.hydrate(),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
// خبرهای سفارش از سمت ادمین میآید، پس شمارندهٔ زنگ بهآرامی تازه میشود
|
||||
useIntervalFn(
|
||||
() => {
|
||||
if (authStore.isAuthenticated) void notificationStore.refreshUnreadCount()
|
||||
},
|
||||
60_000,
|
||||
{ immediate: false },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -5,9 +5,9 @@ defineProps<{
|
||||
|
||||
const steps = [
|
||||
{ num: 1, label: 'سبد خرید' },
|
||||
{ num: 2, label: 'انتخاب تسویه' },
|
||||
{ num: 3, label: 'ثبت اطلاعات' },
|
||||
{ num: 4, label: 'تأیید نهایی' },
|
||||
{ num: 2, label: 'ثبت سفارش' },
|
||||
{ num: 3, label: 'صدور فاکتور' },
|
||||
{ num: 4, label: 'تسویه' },
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,18 +3,27 @@ import { ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useNotificationStore } from '@/stores/notification'
|
||||
import IconSearch from '@/components/icons/IconSearch.vue'
|
||||
import IconBell from '@/components/icons/IconBell.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import { toPersianDigits } from '@/utils/format'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const cartStore = useCartStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
const searchQuery = ref('')
|
||||
const showUserMenu = ref(false)
|
||||
|
||||
const unreadLabel = computed(() =>
|
||||
toPersianDigits(
|
||||
notificationStore.unreadCount > 9 ? '9+' : String(notificationStore.unreadCount),
|
||||
),
|
||||
)
|
||||
|
||||
const initials = computed(() => {
|
||||
const first = authStore.user?.firstName?.[0] ?? ''
|
||||
const last = authStore.user?.lastName?.[0] ?? ''
|
||||
@@ -73,9 +82,20 @@ function logout() {
|
||||
<div class="desktop-header__actions">
|
||||
<span v-if="companyLabel" class="desktop-header__company">{{ companyLabel }}</span>
|
||||
|
||||
<button type="button" class="desktop-header__icon-btn" aria-label="اعلانها">
|
||||
<button
|
||||
type="button"
|
||||
class="desktop-header__icon-btn"
|
||||
aria-label="اعلانها"
|
||||
@click="router.push('/profile/notifications')"
|
||||
>
|
||||
<IconBell :size="20" />
|
||||
<span class="desktop-header__notif-dot" aria-hidden="true" />
|
||||
<span
|
||||
v-if="notificationStore.hasUnread"
|
||||
class="desktop-header__notif-badge"
|
||||
:aria-label="`${notificationStore.unreadCount} اعلان خواندهنشده`"
|
||||
>
|
||||
{{ unreadLabel }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="desktop-header__user-wrap">
|
||||
@@ -200,14 +220,21 @@ function logout() {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.desktop-header__notif-dot {
|
||||
.desktop-header__notif-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
inset-inline-end: 9px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
top: 4px;
|
||||
inset-inline-end: 4px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9px;
|
||||
background: var(--color-danger);
|
||||
color: var(--color-on-primary, #fff);
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
border: 2px solid var(--color-surface);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ const checksTotal = computed(() =>
|
||||
props.order.checks?.reduce((sum, check) => sum + check.amount, 0) ?? 0,
|
||||
)
|
||||
|
||||
function settlementLabel(type: 'cash' | 'check') {
|
||||
function settlementLabel(type: 'cash' | 'check' | null) {
|
||||
if (!type) return 'در انتظار انتخاب مشتری'
|
||||
return type === 'cash' ? 'تسویه نقدی' : 'تسویه چکی'
|
||||
}
|
||||
</script>
|
||||
@@ -19,8 +20,10 @@ function settlementLabel(type: 'cash' | 'check') {
|
||||
<template>
|
||||
<div class="tracking-summary">
|
||||
<div class="tracking-summary__row">
|
||||
<span>مبلغ فاکتور (تقریبی)</span>
|
||||
<strong>{{ order.finalAmount ? formatNumber(order.finalAmount) : '—' }}</strong>
|
||||
<span>مبلغ فاکتور</span>
|
||||
<strong>
|
||||
{{ order.finalAmount ? formatNumber(order.finalAmount) : 'در انتظار قیمتگذاری' }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="tracking-summary__row">
|
||||
<span>نوع تسویه</span>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import { confirm } from '@/composables/useConfirm'
|
||||
|
||||
export function useCartPage() {
|
||||
const router = useRouter()
|
||||
const cartStore = useCartStore()
|
||||
const orderStore = useOrderStore()
|
||||
const showFullDialog = ref(false)
|
||||
const submitting = ref(false)
|
||||
const submitError = ref('')
|
||||
|
||||
const items = computed(() => cartStore.activeCart?.items ?? [])
|
||||
const isEmpty = computed(() => items.value.length === 0)
|
||||
@@ -27,18 +31,37 @@ export function useCartPage() {
|
||||
cartStore.removeItem(itemId)
|
||||
}
|
||||
|
||||
/**
|
||||
* ثبت سفارش بدون انتخاب تسویه؛ قیمتگذاری و انتخاب روش تسویه
|
||||
* بعد از بررسی مدیریت انجام میشود.
|
||||
*/
|
||||
async function submitOrder() {
|
||||
if (!cartStore.activeCart || items.value.length === 0 || submitting.value) return
|
||||
submitting.value = true
|
||||
submitError.value = ''
|
||||
try {
|
||||
const order = await orderStore.submitOrder(cartStore.activeCart)
|
||||
await cartStore.hydrate()
|
||||
router.replace(`/orders/${order.id}/success`)
|
||||
} catch (e) {
|
||||
submitError.value = e instanceof Error ? e.message : 'ثبت سفارش ناموفق بود.'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function proceedToCheckout() {
|
||||
if (items.value.length === 0) return
|
||||
if (cartStore.isFull) {
|
||||
showFullDialog.value = true
|
||||
return
|
||||
}
|
||||
router.push('/checkout/settlement')
|
||||
void submitOrder()
|
||||
}
|
||||
|
||||
function proceedAnyway() {
|
||||
showFullDialog.value = false
|
||||
router.push('/checkout/settlement')
|
||||
void submitOrder()
|
||||
}
|
||||
|
||||
async function createNewInvoice() {
|
||||
@@ -62,6 +85,8 @@ export function useCartPage() {
|
||||
isEmpty,
|
||||
companyName,
|
||||
showFullDialog,
|
||||
submitting,
|
||||
submitError,
|
||||
removeItem,
|
||||
proceedToCheckout,
|
||||
proceedAnyway,
|
||||
|
||||
+31
-30
@@ -46,36 +46,6 @@ const router = createRouter({
|
||||
component: () => import('@/views/cart/CartView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/checkout/settlement',
|
||||
name: 'settlement',
|
||||
component: () => import('@/views/checkout/SettlementTypeView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/checkout/cash',
|
||||
name: 'cash-settlement',
|
||||
component: () => import('@/views/checkout/CashSettlementView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/checkout/check-mode',
|
||||
name: 'check-mode',
|
||||
component: () => import('@/views/checkout/CheckModeSelectView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/checkout/check-form',
|
||||
name: 'check-form',
|
||||
component: () => import('@/views/checkout/CheckFormView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/checkout/check-list',
|
||||
name: 'check-list',
|
||||
component: () => import('@/views/checkout/CheckListView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/orders',
|
||||
name: 'orders',
|
||||
@@ -94,6 +64,37 @@ const router = createRouter({
|
||||
component: () => import('@/views/orders/OrderSuccessView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
// مراحل تسویه بعد از صدور فاکتور، روی همان سفارش
|
||||
{
|
||||
path: '/orders/:id/settlement',
|
||||
name: 'settlement',
|
||||
component: () => import('@/views/checkout/SettlementTypeView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/orders/:id/cash',
|
||||
name: 'cash-settlement',
|
||||
component: () => import('@/views/checkout/CashSettlementView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/orders/:id/check-mode',
|
||||
name: 'check-mode',
|
||||
component: () => import('@/views/checkout/CheckModeSelectView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/orders/:id/check-form',
|
||||
name: 'check-form',
|
||||
component: () => import('@/views/checkout/CheckFormView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/orders/:id/check-list',
|
||||
name: 'check-list',
|
||||
component: () => import('@/views/checkout/CheckListView.vue'),
|
||||
meta: { requiresAuth: true, hideNav: true },
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'profile',
|
||||
|
||||
@@ -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'>>) {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
+22
-6
@@ -98,7 +98,6 @@ export interface CartItem {
|
||||
productCode: string
|
||||
color: string
|
||||
meterage: number
|
||||
unitPrice?: number
|
||||
notes?: string
|
||||
companyId: string
|
||||
companyName: string
|
||||
@@ -175,17 +174,20 @@ export interface Order {
|
||||
companyName: string
|
||||
items: OrderItem[]
|
||||
totalMeterage: number
|
||||
settlementType: SettlementType
|
||||
/** تا وقتی مشتری بعد از قیمتگذاری روش تسویه را انتخاب نکرده، خالی است. */
|
||||
settlementType: SettlementType | null
|
||||
checkMode?: CheckRegistrationMode
|
||||
checks?: CheckData[]
|
||||
checkRassDays?: number
|
||||
checkRassDate?: string
|
||||
status: OrderStatus
|
||||
notes?: string
|
||||
totalAmount?: number
|
||||
shippingCost?: number
|
||||
totalDiscount?: number
|
||||
finalAmount?: number
|
||||
totalAmount?: number | null
|
||||
shippingCost?: number | null
|
||||
totalDiscount?: number | null
|
||||
finalAmount?: number | null
|
||||
/** سرور تا پایان قیمتگذاری مبالغ را null برمیگرداند. */
|
||||
pricesVisible?: boolean
|
||||
cashInvoiceExpiresAt?: string
|
||||
cashReceipt?: CashPaymentReceipt
|
||||
statusHistory: OrderStatusHistoryEntry[]
|
||||
@@ -193,6 +195,20 @@ export interface Order {
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type NotificationTone = 'info' | 'warning' | 'success' | 'danger'
|
||||
|
||||
export interface AppNotification {
|
||||
id: string
|
||||
orderId?: string
|
||||
event: string
|
||||
title: string
|
||||
body: string
|
||||
tone: NotificationTone
|
||||
link?: string
|
||||
isRead: boolean
|
||||
sentAt: string
|
||||
}
|
||||
|
||||
export interface CheckRassResult {
|
||||
checkCount: number
|
||||
totalAmount: number
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useCartPage } from '@/composables/useCartPage'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import MobileCartPage from '@/views/cart/components/MobileCartPage.vue'
|
||||
import DesktopCartPage from '@/views/cart/components/DesktopCartPage.vue'
|
||||
|
||||
@@ -13,6 +14,8 @@ const {
|
||||
isEmpty,
|
||||
companyName,
|
||||
showFullDialog,
|
||||
submitting,
|
||||
submitError,
|
||||
removeItem,
|
||||
proceedToCheckout,
|
||||
proceedAnyway,
|
||||
@@ -31,6 +34,8 @@ function goToOrdering() {
|
||||
|
||||
<template>
|
||||
<div class="cart-view">
|
||||
<FormErrorBanner v-if="submitError" :message="submitError" class="cart-view__error" />
|
||||
|
||||
<MobileCartPage
|
||||
class="mobile-only"
|
||||
:items="items"
|
||||
@@ -39,6 +44,7 @@ function goToOrdering() {
|
||||
:max-meterage="cartStore.maxMeterage"
|
||||
:remaining-capacity="cartStore.remainingCapacity"
|
||||
:is-empty="isEmpty"
|
||||
:submitting="submitting"
|
||||
@back="router.push('/')"
|
||||
@remove="removeItem"
|
||||
@checkout="proceedToCheckout"
|
||||
@@ -53,6 +59,7 @@ function goToOrdering() {
|
||||
:max-meterage="cartStore.maxMeterage"
|
||||
:remaining-capacity="cartStore.remainingCapacity"
|
||||
:is-empty="isEmpty"
|
||||
:submitting="submitting"
|
||||
@remove="removeItem"
|
||||
@checkout="proceedToCheckout"
|
||||
@select-company="router.push('/')"
|
||||
@@ -66,7 +73,9 @@ function goToOrdering() {
|
||||
سقف متراژ این فاکتور تکمیل شده است. آیا مایلید ادامه محصولات را در فاکتور جدید ثبت کنید؟
|
||||
</p>
|
||||
<div class="dialog-actions">
|
||||
<BaseButton variant="outline" @click="proceedAnyway">تسویه همین فاکتور</BaseButton>
|
||||
<BaseButton variant="outline" :loading="submitting" @click="proceedAnyway">
|
||||
ثبت همین فاکتور
|
||||
</BaseButton>
|
||||
<BaseButton @click="createNewInvoice">فاکتور جدید</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,6 +92,10 @@ function goToOrdering() {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cart-view__error {
|
||||
margin: 0 var(--page-padding);
|
||||
}
|
||||
|
||||
.dialog h3 {
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import CheckoutStepper from '@/components/layout/CheckoutStepper.vue'
|
||||
import IconCart from '@/components/icons/IconCart.vue'
|
||||
@@ -11,16 +10,17 @@ import IconClipboardEmpty from '@/components/icons/IconClipboardEmpty.vue'
|
||||
import IconLightbulb from '@/components/icons/IconLightbulb.vue'
|
||||
import IconLayoutGrid from '@/components/icons/IconLayoutGrid.vue'
|
||||
import IconDiamond from '@/components/icons/IconDiamond.vue'
|
||||
import { formatMeterage, formatNumber, formatCurrency } from '@/utils/format'
|
||||
import { formatMeterage, formatNumber } from '@/utils/format'
|
||||
import type { CartItem } from '@/types'
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
items: CartItem[]
|
||||
companyName: string
|
||||
totalMeterage: number
|
||||
maxMeterage: number
|
||||
remainingCapacity: number
|
||||
isEmpty: boolean
|
||||
submitting?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -32,10 +32,6 @@ const emit = defineEmits<{
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const payableAmount = computed(() =>
|
||||
props.items.reduce((sum, item) => sum + (item.unitPrice ?? 0) * item.meterage, 0),
|
||||
)
|
||||
|
||||
const quickLinks = [
|
||||
{
|
||||
title: 'انتخاب شرکت',
|
||||
@@ -160,25 +156,28 @@ const quickLinks = [
|
||||
</span>
|
||||
<strong>{{ formatMeterage(totalMeterage) }}</strong>
|
||||
</div>
|
||||
<div class="cart-summary-card__row">
|
||||
<div class="cart-summary-card__row cart-summary-card__row--hint">
|
||||
<span class="cart-summary-card__row-label">
|
||||
<IconOrders :size="16" />
|
||||
مبلغ قابل پرداخت
|
||||
مبلغ فاکتور
|
||||
</span>
|
||||
<span class="cart-summary-card__hint-value">
|
||||
پس از قیمتگذاری مدیریت
|
||||
</span>
|
||||
<strong>{{
|
||||
payableAmount > 0 ? formatCurrency(payableAmount) : '۰ تومان'
|
||||
}}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="cart-summary-card__cta"
|
||||
:disabled="isEmpty"
|
||||
:disabled="isEmpty || submitting"
|
||||
@click="emit('checkout')"
|
||||
>
|
||||
<IconChevronLeft :size="16" />
|
||||
ادامه و انتخاب تسویه
|
||||
{{ submitting ? 'در حال ثبت…' : 'ثبت سفارش' }}
|
||||
</button>
|
||||
<p class="cart-summary-card__note">
|
||||
قیمتگذاری توسط مدیریت انجام میشود؛ پس از صدور فاکتور روش تسویه را انتخاب میکنید.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="cart-guide-card">
|
||||
@@ -475,7 +474,7 @@ const quickLinks = [
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
font-size: 0.8125rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.cart-summary-card__row:last-child {
|
||||
@@ -494,6 +493,18 @@ const quickLinks = [
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.cart-summary-card__hint-value {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.cart-summary-card__note {
|
||||
margin: 10px 0 0;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.cart-summary-card__cta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -13,6 +13,7 @@ defineProps<{
|
||||
maxMeterage: number
|
||||
remainingCapacity: number
|
||||
isEmpty: boolean
|
||||
submitting?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -78,7 +79,11 @@ const emit = defineEmits<{
|
||||
<strong>{{ formatMeterage(remainingCapacity) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<BaseButton block @click="emit('checkout')">ادامه و انتخاب نوع تسویه</BaseButton>
|
||||
<p class="cart-mobile-page__hint">
|
||||
پس از ثبت، مدیریت سفارش را قیمتگذاری میکند و فاکتور برای انتخاب روش تسویه به شما اعلام
|
||||
میشود.
|
||||
</p>
|
||||
<BaseButton block :loading="submitting" @click="emit('checkout')">ثبت سفارش</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -108,6 +113,13 @@ const emit = defineEmits<{
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cart-mobile-page__hint {
|
||||
margin: 0 0 10px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.cart-mobile-page__title {
|
||||
margin: 0;
|
||||
font-size: 1.0625rem;
|
||||
|
||||
@@ -1,48 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useCatalogStore } from '@/stores/catalog'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import IconSuccessCircle from '@/components/icons/IconSuccessCircle.vue'
|
||||
import { toPersianDigits } from '@/utils/format'
|
||||
import type { BankAccount, Order } from '@/types'
|
||||
import { formatCurrency, formatDateTime } from '@/utils/format'
|
||||
import type { BankAccount } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const cartStore = useCartStore()
|
||||
const orderStore = useOrderStore()
|
||||
const authStore = useAuthStore()
|
||||
const catalogStore = useCatalogStore()
|
||||
const bankAccounts = ref<BankAccount[]>([])
|
||||
const submitting = ref(false)
|
||||
const formError = ref('')
|
||||
const createdOrder = ref<Order | null>(null)
|
||||
|
||||
const companyId = computed(() => {
|
||||
const firstItem = cartStore.activeCart?.items[0]
|
||||
return firstItem?.companyId || cartStore.selectedCompanyId || cartStore.activeCart?.companyId || ''
|
||||
})
|
||||
const companyName = computed(
|
||||
() => cartStore.companyNamesLabel || cartStore.activeCart?.companyName || '',
|
||||
const orderId = computed(() => route.params.id as string)
|
||||
const order = computed(() => orderStore.getOrderById(orderId.value))
|
||||
const companyId = computed(() => order.value?.companyId ?? '')
|
||||
const companyName = computed(() => order.value?.companyName ?? '')
|
||||
|
||||
watch(
|
||||
orderId,
|
||||
async (id) => {
|
||||
if (id && !orderStore.getOrderById(id)) await orderStore.fetchOrder(id)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
if (!catalogStore.getCompany(companyId.value)) {
|
||||
await catalogStore.loadCompanies()
|
||||
}
|
||||
if (companyId.value) {
|
||||
bankAccounts.value = await catalogStore.loadBankAccounts(companyId.value)
|
||||
}
|
||||
})
|
||||
watch(
|
||||
companyId,
|
||||
async (id) => {
|
||||
if (!id) {
|
||||
bankAccounts.value = []
|
||||
return
|
||||
}
|
||||
if (!catalogStore.getCompany(id)) await catalogStore.loadCompanies()
|
||||
bankAccounts.value = await catalogStore.loadBankAccounts(id)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const invoiceLabel = computed(() =>
|
||||
order.value?.finalAmount ? formatCurrency(order.value.finalAmount) : '—',
|
||||
)
|
||||
|
||||
const expiryMessage = computed(() => {
|
||||
const hour = catalogStore.getCompany(companyId.value)?.cashInvoiceValidUntilHour ?? 17
|
||||
const time = toPersianDigits(String(hour).padStart(2, '0'))
|
||||
return `پس از ثبت، فاکتور تا ساعت ${time}:۰۰ امروز معتبر است.`
|
||||
const expiry = order.value?.cashInvoiceExpiresAt
|
||||
if (!expiry) return 'مبلغ فاکتور را به یکی از حسابهای زیر واریز و رسید را ثبت کنید.'
|
||||
return `مهلت پرداخت این فاکتور تا ${formatDateTime(expiry)} است.`
|
||||
})
|
||||
|
||||
const accountsSectionTitle = computed(() => {
|
||||
@@ -61,34 +67,12 @@ function formatIban(value: string) {
|
||||
return clean.replace(/(.{4})/g, '$1 ').trim()
|
||||
}
|
||||
|
||||
async function createOrder(): Promise<Order | null> {
|
||||
if (createdOrder.value) return createdOrder.value
|
||||
if (!authStore.user || !cartStore.activeCart || submitting.value) return null
|
||||
formError.value = ''
|
||||
submitting.value = true
|
||||
try {
|
||||
const order = await orderStore.submitOrder(authStore.user.id, cartStore.activeCart, 'cash')
|
||||
createdOrder.value = order
|
||||
await cartStore.hydrate()
|
||||
return order
|
||||
} catch (e) {
|
||||
formError.value = e instanceof Error ? e.message : 'ثبت سفارش ناموفق بود.'
|
||||
return null
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
function viewOrder() {
|
||||
router.replace(`/orders/${orderId.value}?track=1`)
|
||||
}
|
||||
|
||||
async function viewOrder() {
|
||||
const order = await createOrder()
|
||||
if (!order) return
|
||||
router.replace(`/orders/${order.id}?track=1`)
|
||||
}
|
||||
|
||||
async function registerReceipt() {
|
||||
const order = await createOrder()
|
||||
if (!order) return
|
||||
router.replace(`/orders/${order.id}?track=1&receipt=1`)
|
||||
function registerReceipt() {
|
||||
router.replace(`/orders/${orderId.value}?track=1&receipt=1`)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -99,17 +83,13 @@ async function registerReceipt() {
|
||||
title="تسویه نقدی"
|
||||
show-back
|
||||
centered
|
||||
@back="router.push('/checkout/settlement')"
|
||||
@back="router.push(`/orders/${orderId}`)"
|
||||
/>
|
||||
|
||||
<div class="cash-page__scroll page__scroll">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<div class="cash-page__success" :class="{ 'cash-page__success--done': createdOrder }">
|
||||
<IconSuccessCircle v-if="createdOrder" :size="56" />
|
||||
<p class="cash-page__success-title">
|
||||
{{ createdOrder ? 'فاکتور شما ثبت شد.' : 'آماده ثبت فاکتور نقدی' }}
|
||||
</p>
|
||||
<div class="cash-page__success cash-page__success--done">
|
||||
<IconSuccessCircle :size="56" />
|
||||
<p class="cash-page__success-title">مبلغ قابل پرداخت {{ invoiceLabel }}</p>
|
||||
<p class="cash-page__success-subtitle">{{ expiryMessage }}</p>
|
||||
</div>
|
||||
|
||||
@@ -138,11 +118,9 @@ async function registerReceipt() {
|
||||
</div>
|
||||
|
||||
<div class="cash-page__footer">
|
||||
<BaseButton variant="outline" block :loading="submitting" @click="viewOrder">
|
||||
ثبت و مشاهده سفارش
|
||||
</BaseButton>
|
||||
<BaseButton block class="cash-page__submit" :loading="submitting" @click="registerReceipt">
|
||||
ثبت سفارش و رسید پرداخت
|
||||
<BaseButton variant="outline" block @click="viewOrder">مشاهده سفارش</BaseButton>
|
||||
<BaseButton block class="cash-page__submit" @click="registerReceipt">
|
||||
ثبت رسید پرداخت
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,15 +132,11 @@ async function registerReceipt() {
|
||||
<p class="desktop-page__subtitle">{{ expiryMessage }}</p>
|
||||
</header>
|
||||
|
||||
<div class="cash-page__success" :class="{ 'cash-page__success--done': createdOrder }">
|
||||
<IconSuccessCircle v-if="createdOrder" :size="56" />
|
||||
<p class="cash-page__success-title">
|
||||
{{ createdOrder ? 'فاکتور شما ثبت شد.' : 'آماده ثبت فاکتور نقدی' }}
|
||||
</p>
|
||||
<div class="cash-page__success cash-page__success--done">
|
||||
<IconSuccessCircle :size="56" />
|
||||
<p class="cash-page__success-title">مبلغ قابل پرداخت {{ invoiceLabel }}</p>
|
||||
</div>
|
||||
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<section class="accounts-panel">
|
||||
<h2 class="accounts-panel__title">{{ accountsSectionTitle }}</h2>
|
||||
<div class="desktop-grid-2">
|
||||
@@ -184,11 +158,11 @@ async function registerReceipt() {
|
||||
</section>
|
||||
|
||||
<div class="cash-desktop-page__actions">
|
||||
<BaseButton variant="outline" class="desktop-btn-compact" :loading="submitting" @click="viewOrder">
|
||||
ثبت و مشاهده سفارش
|
||||
<BaseButton variant="outline" class="desktop-btn-compact" @click="viewOrder">
|
||||
مشاهده سفارش
|
||||
</BaseButton>
|
||||
<BaseButton class="desktop-btn-compact" :loading="submitting" @click="registerReceipt">
|
||||
ثبت سفارش و رسید پرداخت
|
||||
<BaseButton class="desktop-btn-compact" @click="registerReceipt">
|
||||
ثبت رسید پرداخت
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import BaseInput from '@/components/ui/BaseInput.vue'
|
||||
@@ -22,9 +22,11 @@ import {
|
||||
fileToBase64,
|
||||
} from '@/utils/validation'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const orderId = computed(() => route.params.id as string)
|
||||
const isFull = computed(() => orderStore.draftCheckMode === 'full')
|
||||
|
||||
const amount = ref('')
|
||||
@@ -137,7 +139,7 @@ function saveCheck() {
|
||||
})
|
||||
|
||||
resetForm()
|
||||
router.push('/checkout/check-list')
|
||||
router.push(`/orders/${orderId.value}/check-list`)
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
@@ -163,7 +165,7 @@ function resetForm() {
|
||||
<PageHeader
|
||||
:title="isFull ? 'ثبت چک - حالت کامل' : 'ثبت چک - حالت مختصر'"
|
||||
show-back
|
||||
@back="router.push('/checkout/check-mode')"
|
||||
@back="router.push(`/orders/${orderId}/check-mode`)"
|
||||
/>
|
||||
|
||||
<div class="page__scroll checkout-page__scroll">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
@@ -15,43 +14,50 @@ import type { CheckData } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const cartStore = useCartStore()
|
||||
const orderStore = useOrderStore()
|
||||
const authStore = useAuthStore()
|
||||
const formError = ref('')
|
||||
const submitting = ref(false)
|
||||
|
||||
const isCheckout = computed(() => route.name === 'check-list')
|
||||
const orderId = computed(() => (route.params.id as string | undefined) ?? '')
|
||||
const order = computed(() =>
|
||||
orderId.value ? orderStore.getOrderById(orderId.value) : undefined,
|
||||
)
|
||||
|
||||
watch(
|
||||
orderId,
|
||||
async (id) => {
|
||||
if (!id || !isCheckout.value) return
|
||||
const current = orderStore.getOrderById(id) ?? (await orderStore.fetchOrder(id))
|
||||
if (!current) return
|
||||
// در مسیر اصلاح، چکهای ثبتشده به فرم برمیگردند تا مشتری ویرایش کند
|
||||
if (orderStore.draftChecks.length === 0 && (current.checks?.length ?? 0) > 0) {
|
||||
orderStore.loadChecksIntoDraft(current)
|
||||
} else if (orderStore.draftOrderId !== id) {
|
||||
orderStore.startCheckDraft(id, current.checkMode ?? undefined)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const checks = computed<CheckData[]>(() => {
|
||||
if (orderStore.draftChecks.length > 0) return orderStore.draftChecks
|
||||
if (isCheckout.value) return order.value?.checks ?? []
|
||||
if (!authStore.user) return []
|
||||
|
||||
return orderStore
|
||||
.userOrders(authStore.user.id)
|
||||
.filter((order) => order.settlementType === 'check' && order.checks?.length)
|
||||
.flatMap((order) => order.checks ?? [])
|
||||
.filter((o) => o.settlementType === 'check' && o.checks?.length)
|
||||
.flatMap((o) => o.checks ?? [])
|
||||
})
|
||||
|
||||
const rass = computed(() => {
|
||||
if (orderStore.draftChecks.length > 0) return orderStore.getDraftRass()
|
||||
if (checks.value.length === 0) {
|
||||
return calculateCheckRass([], new Date())
|
||||
}
|
||||
return calculateCheckRass(checks.value, new Date())
|
||||
})
|
||||
|
||||
const invoiceAmount = computed(() => {
|
||||
if (!authStore.user) return 0
|
||||
|
||||
const activeOrder = orderStore
|
||||
.userOrders(authStore.user.id)
|
||||
.find((order) => order.settlementType === 'check' && order.finalAmount)
|
||||
|
||||
if (activeOrder?.finalAmount) return activeOrder.finalAmount
|
||||
if (isCheckout.value && rass.value.totalAmount > 0) return rass.value.totalAmount
|
||||
return rass.value.totalAmount
|
||||
})
|
||||
const invoiceAmount = computed(() => order.value?.finalAmount ?? 0)
|
||||
|
||||
const isAmountComplete = computed(
|
||||
() => invoiceAmount.value > 0 && rass.value.totalAmount >= invoiceAmount.value,
|
||||
@@ -65,11 +71,11 @@ const openMenuId = computed({
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
router.push(isCheckout.value ? '/checkout/check-form' : '/profile')
|
||||
router.push(isCheckout.value ? `/orders/${orderId.value}` : '/profile')
|
||||
}
|
||||
|
||||
function addAnother() {
|
||||
router.push('/checkout/check-form')
|
||||
router.push(`/orders/${orderId.value}/check-form`)
|
||||
}
|
||||
|
||||
async function removeCheck(id: string) {
|
||||
@@ -89,24 +95,20 @@ function toggleMenu(id: string) {
|
||||
openMenuId.value = openMenuId.value === id ? '' : id
|
||||
}
|
||||
|
||||
async function submitOrder() {
|
||||
if (!authStore.user || !cartStore.activeCart || checks.value.length === 0) return
|
||||
if (submitting.value) return
|
||||
async function submitChecks() {
|
||||
if (!orderId.value || orderStore.draftChecks.length === 0 || submitting.value) return
|
||||
formError.value = ''
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
const order = await orderStore.submitOrder(
|
||||
authStore.user.id,
|
||||
cartStore.activeCart,
|
||||
'check',
|
||||
await orderStore.updateOrderChecks(
|
||||
orderId.value,
|
||||
[...orderStore.draftChecks],
|
||||
orderStore.draftCheckMode ?? 'brief',
|
||||
)
|
||||
await cartStore.hydrate()
|
||||
router.replace(`/orders/${order.id}/success`)
|
||||
router.replace(`/orders/${orderId.value}?track=1`)
|
||||
} catch (e) {
|
||||
formError.value = e instanceof Error ? e.message : 'ثبت سفارش ناموفق بود.'
|
||||
formError.value = e instanceof Error ? e.message : 'ثبت چکها ناموفق بود.'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -203,7 +205,7 @@ async function submitOrder() {
|
||||
block
|
||||
:disabled="checks.length === 0"
|
||||
:loading="submitting"
|
||||
@click="submitOrder"
|
||||
@click="submitChecks"
|
||||
>
|
||||
ارسال برای بررسی
|
||||
</BaseButton>
|
||||
@@ -283,7 +285,7 @@ async function submitOrder() {
|
||||
block
|
||||
:disabled="checks.length === 0"
|
||||
:loading="submitting"
|
||||
@click="submitOrder"
|
||||
@click="submitChecks"
|
||||
>
|
||||
ارسال برای بررسی
|
||||
</BaseButton>
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import IconCheckBook from '@/components/icons/IconCheckBook.vue'
|
||||
import IconEditPen from '@/components/icons/IconEditPen.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const orderId = computed(() => route.params.id as string)
|
||||
|
||||
watch(
|
||||
orderId,
|
||||
async (id) => {
|
||||
if (!id) return
|
||||
if (!orderStore.getOrderById(id)) await orderStore.fetchOrder(id)
|
||||
if (orderStore.draftOrderId !== id) orderStore.startCheckDraft(id)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function selectMode(mode: 'full' | 'brief') {
|
||||
orderStore.setCheckMode(mode)
|
||||
router.push('/checkout/check-form')
|
||||
router.push(`/orders/${orderId.value}/check-form`)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -21,7 +35,7 @@ function selectMode(mode: 'full' | 'brief') {
|
||||
subtitle="نحوه ثبت اطلاعات چکها را انتخاب کنید"
|
||||
show-back
|
||||
centered
|
||||
@back="router.push('/checkout/settlement')"
|
||||
@back="router.push(`/orders/${orderId}`)"
|
||||
/>
|
||||
|
||||
<div class="page__scroll check-mode-page__scroll">
|
||||
|
||||
@@ -1,23 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import CheckoutStepper from '@/components/layout/CheckoutStepper.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import IconCashSettlement from '@/components/icons/IconCashSettlement.vue'
|
||||
import IconCheckBook from '@/components/icons/IconCheckBook.vue'
|
||||
import { formatCurrency } from '@/utils/format'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const cartStore = useCartStore()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
function selectType(type: 'cash' | 'check') {
|
||||
if (!cartStore.activeCart) {
|
||||
router.push('/cart')
|
||||
return
|
||||
const orderId = computed(() => route.params.id as string)
|
||||
const order = computed(() => orderStore.getOrderById(orderId.value))
|
||||
const submitting = ref(false)
|
||||
const formError = ref('')
|
||||
|
||||
watch(
|
||||
orderId,
|
||||
async (id) => {
|
||||
if (id && !orderStore.getOrderById(id)) await orderStore.fetchOrder(id)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const invoiceLabel = computed(() =>
|
||||
order.value?.finalAmount ? formatCurrency(order.value.finalAmount) : '—',
|
||||
)
|
||||
|
||||
async function selectType(type: 'cash' | 'check') {
|
||||
if (submitting.value) return
|
||||
submitting.value = true
|
||||
formError.value = ''
|
||||
try {
|
||||
await orderStore.chooseSettlement(orderId.value, type)
|
||||
if (type === 'cash') {
|
||||
router.replace(`/orders/${orderId.value}/cash`)
|
||||
} else {
|
||||
orderStore.startCheckDraft(orderId.value)
|
||||
router.replace(`/orders/${orderId.value}/check-mode`)
|
||||
}
|
||||
} catch (e) {
|
||||
formError.value = e instanceof Error ? e.message : 'ثبت روش تسویه ناموفق بود.'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
orderStore.startCheckout(cartStore.activeCart, type)
|
||||
router.push(type === 'cash' ? '/checkout/cash' : '/checkout/check-mode')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -29,18 +58,36 @@ function selectType(type: 'cash' | 'check') {
|
||||
subtitle="نوع تسویه را انتخاب کنید"
|
||||
show-back
|
||||
centered
|
||||
@back="router.push('/cart')"
|
||||
@back="router.push(`/orders/${orderId}`)"
|
||||
/>
|
||||
<div class="page__scroll">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<div class="settlement-invoice">
|
||||
<span class="settlement-invoice__label">مبلغ فاکتور</span>
|
||||
<strong class="settlement-invoice__value">{{ invoiceLabel }}</strong>
|
||||
<span v-if="order" class="settlement-invoice__meta">سفارش {{ order.orderNumber }}</span>
|
||||
</div>
|
||||
|
||||
<div class="settlement-mobile-options">
|
||||
<button type="button" class="settlement-card" @click="selectType('cash')">
|
||||
<button
|
||||
type="button"
|
||||
class="settlement-card"
|
||||
:disabled="submitting"
|
||||
@click="selectType('cash')"
|
||||
>
|
||||
<span class="settlement-card__icon settlement-card__icon--cash">
|
||||
<IconCashSettlement :size="36" />
|
||||
</span>
|
||||
<h3>تسویه نقدی</h3>
|
||||
<p>پرداخت از طریق واریز به حساب شرکت</p>
|
||||
</button>
|
||||
<button type="button" class="settlement-card" @click="selectType('check')">
|
||||
<button
|
||||
type="button"
|
||||
class="settlement-card"
|
||||
:disabled="submitting"
|
||||
@click="selectType('check')"
|
||||
>
|
||||
<span class="settlement-card__icon settlement-card__icon--check">
|
||||
<IconCheckBook :size="36" />
|
||||
</span>
|
||||
@@ -53,20 +100,35 @@ function selectType(type: 'cash' | 'check') {
|
||||
|
||||
<!-- Desktop -->
|
||||
<div class="desktop-checkout-shell desktop-only">
|
||||
<CheckoutStepper :current-step="2" />
|
||||
<CheckoutStepper :current-step="4" />
|
||||
<header class="desktop-page__head">
|
||||
<h1 class="desktop-page__title">انتخاب نوع تسویه</h1>
|
||||
<p class="desktop-page__subtitle">نوع تسویه را انتخاب کنید</p>
|
||||
<p class="desktop-page__subtitle">
|
||||
مبلغ فاکتور {{ invoiceLabel }}{{ order ? ` · سفارش ${order.orderNumber}` : '' }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<div class="desktop-settlement-grid">
|
||||
<button type="button" class="settlement-card settlement-card--desktop" @click="selectType('cash')">
|
||||
<button
|
||||
type="button"
|
||||
class="settlement-card settlement-card--desktop"
|
||||
:disabled="submitting"
|
||||
@click="selectType('cash')"
|
||||
>
|
||||
<span class="settlement-card__icon settlement-card__icon--cash">
|
||||
<IconCashSettlement :size="40" />
|
||||
</span>
|
||||
<h3>تسویه نقدی</h3>
|
||||
<p>پرداخت از طریق واریز به حساب شرکت</p>
|
||||
</button>
|
||||
<button type="button" class="settlement-card settlement-card--desktop" @click="selectType('check')">
|
||||
<button
|
||||
type="button"
|
||||
class="settlement-card settlement-card--desktop"
|
||||
:disabled="submitting"
|
||||
@click="selectType('check')"
|
||||
>
|
||||
<span class="settlement-card__icon settlement-card__icon--check">
|
||||
<IconCheckBook :size="40" />
|
||||
</span>
|
||||
@@ -105,6 +167,39 @@ function selectType(type: 'cash' | 'check') {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.settlement-card:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.settlement-invoice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 18px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 16px;
|
||||
background: var(--color-primary-light);
|
||||
border: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.settlement-invoice__label {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.settlement-invoice__value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.settlement-invoice__meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.settlement-card--desktop {
|
||||
min-height: 220px;
|
||||
border: 1px solid var(--color-border);
|
||||
|
||||
@@ -111,10 +111,46 @@ function formatOrderDateTime(date: string) {
|
||||
return `${timePart} - ${datePart}`
|
||||
}
|
||||
|
||||
function settlementLabel(type: 'cash' | 'check') {
|
||||
function settlementLabel(type: 'cash' | 'check' | null) {
|
||||
if (!type) return 'در انتظار انتخاب مشتری'
|
||||
return type === 'cash' ? 'تسویه نقدی' : 'تسویه چکی'
|
||||
}
|
||||
|
||||
type NextAction = { label: string; to?: string; receipt?: boolean }
|
||||
|
||||
/** اقدام بعدی که در توپ زمین مشتری است. */
|
||||
const nextAction = computed<NextAction | null>(() => {
|
||||
const current = order.value
|
||||
if (!current) return null
|
||||
switch (current.status) {
|
||||
case 'awaiting_settlement':
|
||||
case 'awaiting_settlement_fix':
|
||||
return { label: 'انتخاب روش تسویه', to: `/orders/${current.id}/settlement` }
|
||||
case 'check_in_progress':
|
||||
return { label: 'ثبت چکها', to: `/orders/${current.id}/check-mode` }
|
||||
case 'check_needs_fix':
|
||||
case 'check_rejected':
|
||||
return { label: 'اصلاح چکها', to: `/orders/${current.id}/check-list` }
|
||||
case 'awaiting_cash_payment':
|
||||
case 'cash_invoice_issued':
|
||||
return { label: 'ثبت رسید پرداخت', receipt: true }
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
function runNextAction() {
|
||||
const action = nextAction.value
|
||||
if (!action) return
|
||||
if (action.receipt) {
|
||||
showTracking.value = true
|
||||
showOrderDetails.value = true
|
||||
showReceiptForm.value = true
|
||||
return
|
||||
}
|
||||
if (action.to) router.push(action.to)
|
||||
}
|
||||
|
||||
function openTracking() {
|
||||
showTracking.value = true
|
||||
showOrderDetails.value = false
|
||||
@@ -236,11 +272,18 @@ async function submitReceipt() {
|
||||
<div class="order-info-card__row order-info-card__row--last">
|
||||
<span class="order-info-card__key">مبلغ فاکتور</span>
|
||||
<strong class="order-info-card__amount">
|
||||
{{ order.finalAmount ? formatNumber(order.finalAmount) : '—' }}
|
||||
{{ order.finalAmount ? formatNumber(order.finalAmount) : 'پس از قیمتگذاری مدیریت' }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="nextAction" class="order-next-action">
|
||||
<span class="order-next-action__text">برای ادامه سفارش، این مرحله باقی مانده است.</span>
|
||||
<BaseButton class="order-next-action__btn" @click="runNextAction">
|
||||
{{ nextAction.label }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div v-if="showTracking" class="order-detail-page__tracking">
|
||||
<button type="button" class="order-detail-back desktop-only" @click="router.push('/orders')">
|
||||
← بازگشت به سفارشها
|
||||
@@ -286,11 +329,11 @@ async function submitReceipt() {
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
v-if="!order.cashReceipt && order.status !== 'expired'"
|
||||
v-if="nextAction?.receipt"
|
||||
block
|
||||
@click="showReceiptForm = true"
|
||||
>
|
||||
ثبت رسید پرداخت
|
||||
{{ order.cashReceipt ? 'ثبت رسید جدید' : 'ثبت رسید پرداخت' }}
|
||||
</BaseButton>
|
||||
|
||||
<div v-if="order.cashReceipt" class="tracking-card">
|
||||
@@ -550,6 +593,36 @@ async function submitReceipt() {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.order-next-action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
background: var(--color-primary-light);
|
||||
border: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.order-next-action__text {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.order-next-action {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.order-next-action__btn {
|
||||
width: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.order-detail-page__footer {
|
||||
flex-shrink: 0;
|
||||
padding: 12px 0;
|
||||
|
||||
@@ -18,8 +18,8 @@ const order = computed(() => orderStore.getOrderById(route.params.id as string))
|
||||
<div class="success-icon">✓</div>
|
||||
<h1 class="page-title text-center">سفارش ثبت شد</h1>
|
||||
<p class="text-center text-muted mb-4">
|
||||
سفارش شما با موفقیت ثبت شد. اطلاعات محصولات و شرایط تسویه توسط واحد فروش بررسی شده و نتیجه از
|
||||
طریق پیامک و پنل کاربری اعلام میشود.
|
||||
سفارش شما با موفقیت ثبت شد. کارشناسان ما سفارش را بررسی و قیمتگذاری میکنند و بهمحض صدور
|
||||
فاکتور، از طریق پیامک و پنل کاربری به شما اطلاع میدهیم تا روش تسویه را انتخاب کنید.
|
||||
</p>
|
||||
|
||||
<div v-if="order" class="card mb-4">
|
||||
@@ -36,8 +36,8 @@ const order = computed(() => orderStore.getOrderById(route.params.id as string))
|
||||
<span>{{ formatMeterage(order.totalMeterage) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span>نوع تسویه:</span>
|
||||
<span>{{ order.settlementType === 'cash' ? 'نقدی' : 'چکی' }}</span>
|
||||
<span>وضعیت:</span>
|
||||
<span>در انتظار قیمتگذاری</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,118 +1,190 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import EmptyState from '@/components/ui/EmptyState.vue'
|
||||
import ProfileSubpageLayout from '@/views/profile/components/ProfileSubpageLayout.vue'
|
||||
import { useNotificationStore } from '@/stores/notification'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import type { AppNotification, NotificationTone } from '@/types'
|
||||
|
||||
const emailOrders = ref(true)
|
||||
const emailChecks = ref(true)
|
||||
const smsOrders = ref(true)
|
||||
const smsMarketing = ref(false)
|
||||
const saved = ref(false)
|
||||
const router = useRouter()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
function save() {
|
||||
saved.value = true
|
||||
setTimeout(() => {
|
||||
saved.value = false
|
||||
}, 2000)
|
||||
const items = computed(() => notificationStore.items)
|
||||
const hasUnread = computed(() => notificationStore.hasUnread)
|
||||
|
||||
const toneIcons: Record<NotificationTone, string> = {
|
||||
info: 'vuesax-linear:information',
|
||||
success: 'vuesax-linear:tick-circle',
|
||||
warning: 'vuesax-linear:timer',
|
||||
danger: 'vuesax-linear:close-circle',
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void notificationStore.hydrate()
|
||||
})
|
||||
|
||||
async function open(item: AppNotification) {
|
||||
await notificationStore.markRead(item.id)
|
||||
if (item.link) router.push(item.link)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProfileSubpageLayout title="اعلانها" subtitle="مدیریت اعلانهای ایمیل و پیامک">
|
||||
<div class="notify-list">
|
||||
<label class="notify-row">
|
||||
<span class="notify-row__text">
|
||||
<strong>ایمیل وضعیت سفارش</strong>
|
||||
<span>اطلاع از تأیید، ارسال و تکمیل سفارش</span>
|
||||
<ProfileSubpageLayout title="اعلانها" subtitle="آخرین خبرهای سفارشهای شما">
|
||||
<div v-if="items.length" class="notif-box">
|
||||
<div class="notif-box__head">
|
||||
<span class="notif-box__count">
|
||||
{{ hasUnread ? `${notificationStore.unreadCount} اعلان خواندهنشده` : 'همه خوانده شده' }}
|
||||
</span>
|
||||
<input v-model="emailOrders" type="checkbox" class="notify-row__switch" />
|
||||
</label>
|
||||
<BaseButton
|
||||
v-if="hasUnread"
|
||||
variant="outline"
|
||||
class="notif-box__mark"
|
||||
@click="notificationStore.markAllRead()"
|
||||
>
|
||||
خواندن همه
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<label class="notify-row">
|
||||
<span class="notify-row__text">
|
||||
<strong>ایمیل سررسید چک</strong>
|
||||
<span>یادآوری قبل از موعد چکهای ثبتشده</span>
|
||||
<button
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="notif-row"
|
||||
:class="[`notif-row--${item.tone}`, { 'notif-row--unread': !item.isRead }]"
|
||||
@click="open(item)"
|
||||
>
|
||||
<span class="notif-row__icon" aria-hidden="true">
|
||||
<BasicIcon :icon="toneIcons[item.tone]" :size="22" :stroke-width="1.5" />
|
||||
</span>
|
||||
<input v-model="emailChecks" type="checkbox" class="notify-row__switch" />
|
||||
</label>
|
||||
|
||||
<label class="notify-row">
|
||||
<span class="notify-row__text">
|
||||
<strong>پیامک وضعیت سفارش</strong>
|
||||
<span>ارسال پیامک برای تغییر وضعیت سفارش</span>
|
||||
<span class="notif-row__body">
|
||||
<strong class="notif-row__title">{{ item.title }}</strong>
|
||||
<span class="notif-row__text">{{ item.body }}</span>
|
||||
<span class="notif-row__time">{{ formatDateTime(item.sentAt) }}</span>
|
||||
</span>
|
||||
<input v-model="smsOrders" type="checkbox" class="notify-row__switch" />
|
||||
</label>
|
||||
|
||||
<label class="notify-row">
|
||||
<span class="notify-row__text">
|
||||
<strong>پیامکهای تبلیغاتی</strong>
|
||||
<span>تخفیفها و پیشنهادهای ویژه</span>
|
||||
</span>
|
||||
<input v-model="smsMarketing" type="checkbox" class="notify-row__switch" />
|
||||
</label>
|
||||
<span v-if="!item.isRead" class="notif-row__dot" aria-label="خواندهنشده" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<BaseButton block class="notify-save" @click="save">ذخیره تنظیمات</BaseButton>
|
||||
<p v-if="saved" class="notify-saved">تنظیمات اعلان ذخیره شد ✓</p>
|
||||
<EmptyState
|
||||
v-else
|
||||
icon="vuesax-linear:notification-bing"
|
||||
title="اعلانی ندارید"
|
||||
message="وقتی فاکتور سفارشتان صادر شود یا وضعیت چک و پرداخت تغییر کند، همینجا خبردار میشوید."
|
||||
>
|
||||
<BaseButton variant="outline" @click="router.push('/orders')">سفارشهای من</BaseButton>
|
||||
</EmptyState>
|
||||
</ProfileSubpageLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.notify-list {
|
||||
.notif-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.notify-row {
|
||||
.notif-box__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
gap: 12px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.notif-box__count {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.notif-box__mark {
|
||||
width: auto;
|
||||
padding-inline: 14px;
|
||||
}
|
||||
|
||||
.notif-row {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr auto;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
text-align: right;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 14px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.notify-row:last-of-type {
|
||||
border-bottom: none;
|
||||
.notif-row:hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.notify-row__text {
|
||||
.notif-row--unread {
|
||||
background: var(--color-elevated);
|
||||
}
|
||||
|
||||
.notif-row__icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-info-light);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.notif-row--success .notif-row__icon {
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.notif-row--warning .notif-row__icon {
|
||||
background: var(--color-warning-light);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.notif-row--danger .notif-row__icon {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.notif-row__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.notify-row__text strong {
|
||||
.notif-row__title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.notify-row__text span {
|
||||
font-size: 0.75rem;
|
||||
.notif-row__text {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.notify-row__switch {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
.notif-row__time {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.notif-row__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-top: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-danger);
|
||||
flex-shrink: 0;
|
||||
accent-color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notify-save {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.notify-saved {
|
||||
margin: 8px 0 0;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
color: var(--color-success);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -111,7 +111,7 @@ const settingsItems: Array<{
|
||||
{
|
||||
id: 'notifications',
|
||||
label: 'اعلانها',
|
||||
description: 'مدیریت اعلانهای ایمیل و پیامک',
|
||||
description: 'خبرهای فاکتور، چک و ارسال سفارش',
|
||||
icon: IconBell,
|
||||
path: '/profile/notifications',
|
||||
},
|
||||
|
||||
@@ -3,6 +3,8 @@ import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import { useNotificationStore } from '@/stores/notification'
|
||||
import IconBell from '@/components/icons/IconBell.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconProfile from '@/components/icons/IconProfile.vue'
|
||||
import IconCheckBook from '@/components/icons/IconCheckBook.vue'
|
||||
@@ -12,10 +14,19 @@ import IconSettings from '@/components/icons/IconSettings.vue'
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const orderStore = useOrderStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
const draftCheckCount = computed(() => orderStore.draftChecks.length)
|
||||
|
||||
const menuItems = computed(() => [
|
||||
{
|
||||
id: 'notifications',
|
||||
label: 'اعلانها',
|
||||
description: 'خبرهای فاکتور، چک و ارسال',
|
||||
path: '/profile/notifications',
|
||||
icon: IconBell,
|
||||
badge: notificationStore.unreadCount,
|
||||
},
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'اطلاعات کاربری',
|
||||
|
||||
Reference in New Issue
Block a user