Add invoice send, status transitions, and live settings/notifications.
Let admins send priced invoices, change order status via allowedTransitions or forced manual jumps, persist notification reads, and save SystemSettings to the API. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
|
||||
import { onClickOutside, useEventListener } from '@vueuse/core'
|
||||
import AdminIcon from '@/components/icons/AdminIcon.vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { dashboardApi } from '@/services/api'
|
||||
import { dashboardApi, notificationsApi } from '@/services/api'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useThemeStore, type ThemePreference } from '@/stores/theme'
|
||||
@@ -77,7 +77,11 @@ function markMessageRead(message: InternalMessage) {
|
||||
}
|
||||
|
||||
function markNotificationRead(notification: AdminNotification) {
|
||||
if (notification.isRead) return
|
||||
notification.isRead = true
|
||||
void notificationsApi.markRead(notification.id).catch(() => {
|
||||
notification.isRead = false
|
||||
})
|
||||
}
|
||||
|
||||
function markAllMessagesRead() {
|
||||
@@ -87,9 +91,16 @@ function markAllMessagesRead() {
|
||||
}
|
||||
|
||||
function markAllNotificationsRead() {
|
||||
notificationList.value.forEach((n) => {
|
||||
const previouslyUnread = notificationList.value.filter((n) => !n.isRead)
|
||||
if (!previouslyUnread.length) return
|
||||
previouslyUnread.forEach((n) => {
|
||||
n.isRead = true
|
||||
})
|
||||
void notificationsApi.markAllRead().catch(() => {
|
||||
previouslyUnread.forEach((n) => {
|
||||
n.isRead = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function openNotification(notification: AdminNotification) {
|
||||
|
||||
@@ -2,7 +2,8 @@ import { computed, inject, onMounted, provide, reactive, ref, type InjectionKey
|
||||
import { useRouter } from 'vue-router'
|
||||
import { companiesApi, mapAdminOrder, ordersApi } from '@/services/api'
|
||||
import { ApiError } from '@/services/http'
|
||||
import type { Company, Order, SettlementMethod } from '@/types'
|
||||
import type { Company, Order, RawOrderStatus, SettlementMethod } from '@/types'
|
||||
import { rawStatusLabel } from '@/utils/orderStatus'
|
||||
|
||||
import { useToast } from '@/composables/useToast'
|
||||
|
||||
@@ -93,10 +94,42 @@ export function useOrdersPage() {
|
||||
showDetailModal.value = false
|
||||
}
|
||||
|
||||
function settlementLabel(method: string) {
|
||||
function settlementLabel(method?: string | null) {
|
||||
if (!method) return 'انتخابنشده'
|
||||
return method === 'check' ? 'چکی' : 'نقدی'
|
||||
}
|
||||
|
||||
const statusSaving = ref(false)
|
||||
|
||||
async function changeStatus(payload: {
|
||||
orderId: string
|
||||
status: RawOrderStatus
|
||||
note?: string
|
||||
reason?: string
|
||||
force?: boolean
|
||||
}) {
|
||||
if (statusSaving.value) return null
|
||||
statusSaving.value = true
|
||||
try {
|
||||
const updated = await ordersApi.updateStatus(payload.orderId, {
|
||||
status: payload.status,
|
||||
note: payload.note,
|
||||
reason: payload.reason,
|
||||
force: payload.force,
|
||||
})
|
||||
const mapped = mapAdminOrder(updated) as Order
|
||||
const index = orderList.value.findIndex((o) => o.id === payload.orderId)
|
||||
if (index >= 0) orderList.value[index] = { ...orderList.value[index]!, ...mapped }
|
||||
toast.success(`وضعیت سفارش به «${rawStatusLabel(payload.status)}» تغییر کرد.`)
|
||||
return mapped
|
||||
} catch (err) {
|
||||
toast.error(err instanceof ApiError ? err.message : 'تغییر وضعیت انجام نشد.')
|
||||
return null
|
||||
} finally {
|
||||
statusSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goPricing(order: Order) {
|
||||
router.push(`/orders/${order.id}/pricing`)
|
||||
}
|
||||
@@ -105,6 +138,10 @@ export function useOrdersPage() {
|
||||
router.push(`/orders/${order.id}/checks`)
|
||||
}
|
||||
|
||||
function goCashReceipt(order: Order) {
|
||||
router.push({ path: '/cash-payments', query: { order: order.orderNumber } })
|
||||
}
|
||||
|
||||
function openAddModal() {
|
||||
Object.assign(form, emptyForm())
|
||||
formError.value = ''
|
||||
@@ -162,8 +199,11 @@ export function useOrdersPage() {
|
||||
closeDetailPanel,
|
||||
closeDetailModal,
|
||||
settlementLabel,
|
||||
changeStatus,
|
||||
statusSaving,
|
||||
goPricing,
|
||||
goChecks,
|
||||
goCashReceipt,
|
||||
openAddModal,
|
||||
closeAddModal,
|
||||
submitAddOrder,
|
||||
|
||||
+72
-5
@@ -14,6 +14,7 @@ import type {
|
||||
OrderItem,
|
||||
OrderStatus,
|
||||
Product,
|
||||
RawOrderStatus,
|
||||
SettlementMethod,
|
||||
Shipment,
|
||||
ShipmentStatus,
|
||||
@@ -21,13 +22,33 @@ import type {
|
||||
} from '@/types'
|
||||
|
||||
/** Normalize admin list order status badges */
|
||||
export function mapAdminOrder<T extends { status: string; adminStatus?: string; settlementMethod?: string; settlementType?: string; meterage?: number; totalMeterage?: number }>(
|
||||
export function mapAdminOrder<
|
||||
T extends {
|
||||
status: string
|
||||
adminStatus?: string
|
||||
allowedTransitions?: string[]
|
||||
settlementMethod?: string | null
|
||||
settlementType?: string | null
|
||||
meterage?: number
|
||||
totalMeterage?: number
|
||||
},
|
||||
>(
|
||||
order: T,
|
||||
): T & { status: OrderStatus; settlementMethod: SettlementMethod; meterage: number } {
|
||||
): T & {
|
||||
status: OrderStatus
|
||||
rawStatus: RawOrderStatus
|
||||
allowedTransitions: RawOrderStatus[]
|
||||
settlementMethod: SettlementMethod | null
|
||||
meterage: number
|
||||
} {
|
||||
return {
|
||||
...order,
|
||||
status: (order.adminStatus ?? order.status) as OrderStatus,
|
||||
settlementMethod: (order.settlementMethod ?? order.settlementType ?? 'check') as SettlementMethod,
|
||||
rawStatus: order.status as RawOrderStatus,
|
||||
allowedTransitions: (order.allowedTransitions ?? []) as RawOrderStatus[],
|
||||
settlementMethod: (order.settlementMethod ??
|
||||
order.settlementType ??
|
||||
null) as SettlementMethod | null,
|
||||
meterage: order.meterage ?? order.totalMeterage ?? 0,
|
||||
}
|
||||
}
|
||||
@@ -213,7 +234,7 @@ export type UpdateOrderPricingPayload = {
|
||||
|
||||
export type ApiOrder = Order & {
|
||||
adminStatus?: string
|
||||
settlementType?: SettlementMethod
|
||||
settlementType?: SettlementMethod | null
|
||||
totalMeterage?: number
|
||||
shippingCost?: number | null
|
||||
finalAmount?: number | null
|
||||
@@ -221,6 +242,17 @@ export type ApiOrder = Order & {
|
||||
checkRassDate?: string
|
||||
items?: OrderItem[]
|
||||
checks?: Check[]
|
||||
/** وضعیتهای مجاز بعدی از دید ماشین وضعیت سرور */
|
||||
allowedTransitions?: string[]
|
||||
cashReceipt?: { id: string } | null
|
||||
}
|
||||
|
||||
export type UpdateOrderStatusPayload = {
|
||||
status: string
|
||||
note?: string
|
||||
reason?: string
|
||||
/** گذر خارج از ماشین وضعیت؛ سرور reason را اجباری میکند */
|
||||
force?: boolean
|
||||
}
|
||||
|
||||
export const ordersApi = {
|
||||
@@ -230,7 +262,7 @@ export const ordersApi = {
|
||||
create: (body: AdminCreateOrderPayload) => apiRequest<ApiOrder>('/admin/orders', { body }),
|
||||
updatePricing: (id: string, body: UpdateOrderPricingPayload) =>
|
||||
apiRequest<ApiOrder>(`/admin/orders/${id}/pricing`, { method: 'PATCH', body }),
|
||||
updateStatus: (id: string, body: { status: string; note?: string; reason?: string }) =>
|
||||
updateStatus: (id: string, body: UpdateOrderStatusPayload) =>
|
||||
apiRequest<ApiOrder>(`/admin/orders/${id}/status`, { method: 'PATCH', body }),
|
||||
}
|
||||
|
||||
@@ -330,3 +362,38 @@ export const dashboardApi = {
|
||||
get: () => apiRequest<DashboardResponse>('/admin/dashboard'),
|
||||
reports: () => apiRequest<ReportsResponse>('/admin/reports'),
|
||||
}
|
||||
|
||||
/* ── System settings ──────────────────────────────────────── */
|
||||
|
||||
export type SystemSettings = {
|
||||
id: string
|
||||
maxMeteragePerInvoice: number
|
||||
cashInvoiceValidityHours: number
|
||||
defaultSettlementCheck: boolean
|
||||
defaultSettlementCash: boolean
|
||||
smsOrderRegistered: boolean
|
||||
smsPricingReady: boolean
|
||||
smsPaymentReminder: boolean
|
||||
smsCheckReview: boolean
|
||||
smsShippingUpdate: boolean
|
||||
supportPhone: string | null
|
||||
supportEmail: string | null
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type UpdateSettingsPayload = Partial<Omit<SystemSettings, 'id' | 'updatedAt'>>
|
||||
|
||||
export const settingsApi = {
|
||||
get: () => apiRequest<SystemSettings>('/admin/settings'),
|
||||
update: (body: UpdateSettingsPayload) =>
|
||||
apiRequest<SystemSettings>('/admin/settings', { method: 'PATCH', body }),
|
||||
}
|
||||
|
||||
/* ── Admin notifications ──────────────────────────────────── */
|
||||
|
||||
export const notificationsApi = {
|
||||
markRead: (id: string) =>
|
||||
apiRequest<{ ok: boolean }>(`/admin/notifications/${id}/read`, { method: 'PATCH' }),
|
||||
markAllRead: () =>
|
||||
apiRequest<{ ok: boolean }>('/admin/notifications/read-all', { method: 'PATCH' }),
|
||||
}
|
||||
|
||||
+35
-1
@@ -1,6 +1,9 @@
|
||||
/** سطلهای نمایشی پنل ادمین که سرور از وضعیت واقعی سفارش میسازد */
|
||||
export type OrderStatus =
|
||||
| 'pending_review'
|
||||
| 'awaiting_customer'
|
||||
| 'pending_check'
|
||||
| 'pending_payment'
|
||||
| 'needs_correction'
|
||||
| 'check_correction'
|
||||
| 'final_approval'
|
||||
@@ -10,6 +13,35 @@ export type OrderStatus =
|
||||
| 'rejected'
|
||||
| 'expired'
|
||||
|
||||
/** وضعیت واقعی سفارش در ماشین وضعیت سرور */
|
||||
export type RawOrderStatus =
|
||||
| 'cart'
|
||||
| 'submitted'
|
||||
| 'awaiting_review'
|
||||
| 'pricing'
|
||||
| 'awaiting_settlement'
|
||||
| 'awaiting_settlement_fix'
|
||||
| 'cash_in_progress'
|
||||
| 'cash_invoice_issued'
|
||||
| 'awaiting_cash_payment'
|
||||
| 'cash_payment_submitted'
|
||||
| 'awaiting_cash_confirmation'
|
||||
| 'cash_confirmed'
|
||||
| 'check_in_progress'
|
||||
| 'awaiting_check_review'
|
||||
| 'check_needs_fix'
|
||||
| 'check_rejected'
|
||||
| 'checks_approved'
|
||||
| 'final_approved'
|
||||
| 'preparing'
|
||||
| 'ready_to_ship'
|
||||
| 'shipped'
|
||||
| 'delivered'
|
||||
| 'needs_correction'
|
||||
| 'cancelled'
|
||||
| 'rejected'
|
||||
| 'expired'
|
||||
|
||||
export type SettlementMethod = 'check' | 'cash'
|
||||
|
||||
export type CheckStatus = 'pending_review' | 'approved' | 'rejected'
|
||||
@@ -52,10 +84,12 @@ export interface Order {
|
||||
customerName: string
|
||||
companyName: string
|
||||
companyId: string
|
||||
settlementMethod: SettlementMethod
|
||||
settlementMethod: SettlementMethod | null
|
||||
totalAmount: number | null
|
||||
meterage: number
|
||||
status: OrderStatus
|
||||
rawStatus?: RawOrderStatus
|
||||
allowedTransitions?: RawOrderStatus[]
|
||||
createdAt: string
|
||||
customerPhone?: string
|
||||
customerNationalId?: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OrderStatus } from '@/types'
|
||||
import type { OrderStatus, RawOrderStatus } from '@/types'
|
||||
|
||||
export interface StatusMeta {
|
||||
label: string
|
||||
@@ -7,7 +7,9 @@ export interface StatusMeta {
|
||||
|
||||
export const ORDER_STATUS_META: Record<OrderStatus, StatusMeta> = {
|
||||
pending_review: { label: 'در انتظار بررسی', tone: 'warning' },
|
||||
awaiting_customer: { label: 'در انتظار مشتری', tone: 'info' },
|
||||
pending_check: { label: 'در انتظار چک', tone: 'purple' },
|
||||
pending_payment: { label: 'بررسی رسید پرداخت', tone: 'purple' },
|
||||
needs_correction: { label: 'نیاز به اصلاح', tone: 'danger' },
|
||||
check_correction: { label: 'نیاز به اصلاح چک', tone: 'warning' },
|
||||
final_approval: { label: 'تأیید نهایی', tone: 'success' },
|
||||
@@ -18,12 +20,49 @@ export const ORDER_STATUS_META: Record<OrderStatus, StatusMeta> = {
|
||||
expired: { label: 'منقضی شده', tone: 'neutral' },
|
||||
}
|
||||
|
||||
/** برچسب وضعیت واقعی سفارش؛ باید با ORDER_STATUS_LABELS سرور یکسان بماند. */
|
||||
export const RAW_ORDER_STATUS_LABELS: Record<RawOrderStatus, string> = {
|
||||
cart: 'سبد خرید',
|
||||
submitted: 'ثبتشده',
|
||||
awaiting_review: 'در انتظار بررسی',
|
||||
pricing: 'قیمتگذاری',
|
||||
awaiting_settlement: 'در انتظار تسویه',
|
||||
awaiting_settlement_fix: 'اصلاح تسویه',
|
||||
cash_in_progress: 'در حال ثبت نقدی',
|
||||
cash_invoice_issued: 'صدور فاکتور نقدی',
|
||||
awaiting_cash_payment: 'در انتظار پرداخت نقدی',
|
||||
cash_payment_submitted: 'رسید نقدی ارسالشده',
|
||||
awaiting_cash_confirmation: 'تأیید پرداخت نقدی',
|
||||
cash_confirmed: 'پرداخت نقدی تأیید شد',
|
||||
check_in_progress: 'در حال ثبت چک',
|
||||
awaiting_check_review: 'بررسی چک',
|
||||
check_needs_fix: 'اصلاح چک',
|
||||
check_rejected: 'رد چک',
|
||||
checks_approved: 'تأیید چکها',
|
||||
final_approved: 'تأیید نهایی',
|
||||
preparing: 'آمادهسازی',
|
||||
ready_to_ship: 'آماده ارسال',
|
||||
shipped: 'ارسالشده',
|
||||
delivered: 'تحویلشده',
|
||||
needs_correction: 'نیاز به اصلاح',
|
||||
cancelled: 'لغو شده',
|
||||
rejected: 'رد شده',
|
||||
expired: 'منقضی',
|
||||
}
|
||||
|
||||
export function rawStatusLabel(status?: string): string {
|
||||
if (!status) return '—'
|
||||
return RAW_ORDER_STATUS_LABELS[status as RawOrderStatus] ?? status
|
||||
}
|
||||
|
||||
export const ORDER_TABS = [
|
||||
{ key: 'all', label: 'همه' },
|
||||
{ key: 'pending_review', label: 'در انتظار بررسی' },
|
||||
{ key: 'awaiting_customer', label: 'در انتظار مشتری' },
|
||||
{ key: 'pending_check', label: 'در انتظار چک' },
|
||||
{ key: 'needs_correction', label: 'نیاز به اصلاح' },
|
||||
{ key: 'pending_payment', label: 'بررسی رسید' },
|
||||
{ key: 'check_correction', label: 'اصلاح چک' },
|
||||
{ key: 'needs_correction', label: 'نیاز به اصلاح' },
|
||||
{ key: 'final_approval', label: 'تأیید نهایی' },
|
||||
{ key: 'shipped', label: 'ارسال شده' },
|
||||
{ key: 'delivered', label: 'تحویل شده' },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import AdminIcon from '@/components/icons/AdminIcon.vue'
|
||||
import { cashPaymentsApi } from '@/services/api'
|
||||
import { ApiError } from '@/services/http'
|
||||
@@ -11,8 +12,9 @@ import { formatCurrency, formatDateTime } from '@/utils/format'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import type { CashPayment } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
const paymentList = ref<CashPayment[]>([])
|
||||
const searchQuery = ref('')
|
||||
const searchQuery = ref((route.query.order as string) ?? '')
|
||||
const statusFilter = ref<'all' | CashPayment['status']>('all')
|
||||
const toast = useToast()
|
||||
const busyId = ref<string | null>(null)
|
||||
|
||||
@@ -23,6 +23,7 @@ const customerNote = ref('')
|
||||
const showAddModal = ref(false)
|
||||
const formError = ref('')
|
||||
const saving = ref(false)
|
||||
const sending = ref(false)
|
||||
const loading = ref(true)
|
||||
|
||||
const emptyForm = () => ({
|
||||
@@ -46,6 +47,15 @@ const displayOrder = computed(() =>
|
||||
order.value ? mapAdminOrder(order.value) : null,
|
||||
)
|
||||
|
||||
/** تا وقتی سفارش در این وضعیتهاست، فاکتور برای مشتری ارسال نشده است. */
|
||||
const PRE_INVOICE_STATUSES = ['cart', 'submitted', 'awaiting_review', 'pricing', 'needs_correction']
|
||||
|
||||
const invoiceSent = computed(() =>
|
||||
order.value ? !PRE_INVOICE_STATUSES.includes(order.value.status) : false,
|
||||
)
|
||||
|
||||
const canSendInvoice = computed(() => !!order.value?.finalAmount && !invoiceSent.value)
|
||||
|
||||
async function loadOrder() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -160,6 +170,36 @@ async function savePricing() {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function sendInvoice() {
|
||||
if (!order.value) return
|
||||
if (!order.value.finalAmount) {
|
||||
toast.error('ابتدا قیمتگذاری را ذخیره کنید تا مبلغ فاکتور مشخص شود.')
|
||||
return
|
||||
}
|
||||
|
||||
const ok = await confirm({
|
||||
title: 'ارسال فاکتور برای مشتری',
|
||||
message: `فاکتور ${formatCurrency(order.value.finalAmount)} برای مشتری ارسال میشود و او روش تسویه را انتخاب میکند. ادامه میدهید؟`,
|
||||
confirmLabel: 'ارسال فاکتور',
|
||||
cancelLabel: 'انصراف',
|
||||
})
|
||||
if (!ok) return
|
||||
|
||||
sending.value = true
|
||||
try {
|
||||
const updated = await ordersApi.updateStatus(order.value.id, {
|
||||
status: 'awaiting_settlement',
|
||||
note: customerNote.value.trim() || undefined,
|
||||
})
|
||||
order.value = updated
|
||||
toast.success('فاکتور برای مشتری ارسال شد.')
|
||||
} catch (err) {
|
||||
toast.error(err instanceof ApiError ? err.message : 'ارسال فاکتور انجام نشد.')
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -176,9 +216,17 @@ async function savePricing() {
|
||||
:disabled="saving || loading"
|
||||
@click="savePricing"
|
||||
>
|
||||
ارسال پیشفاکتور
|
||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره قیمتگذاری' }}
|
||||
</button>
|
||||
<button type="button" class="admin-btn admin-btn--primary admin-btn--sm" @click="goBack">
|
||||
<button
|
||||
type="button"
|
||||
class="admin-btn admin-btn--primary admin-btn--sm"
|
||||
:disabled="!canSendInvoice || sending || saving"
|
||||
@click="sendInvoice"
|
||||
>
|
||||
{{ sending ? 'در حال ارسال…' : 'ارسال فاکتور برای مشتری' }}
|
||||
</button>
|
||||
<button type="button" class="admin-btn admin-btn--outline admin-btn--sm" @click="goBack">
|
||||
بازگشت
|
||||
</button>
|
||||
</div>
|
||||
@@ -326,11 +374,29 @@ async function savePricing() {
|
||||
:disabled="saving"
|
||||
@click="savePricing"
|
||||
>
|
||||
ارسال پیشفاکتور
|
||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره قیمتگذاری' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="admin-btn admin-btn--primary"
|
||||
:disabled="!canSendInvoice || sending || saving"
|
||||
@click="sendInvoice"
|
||||
>
|
||||
{{ sending ? 'در حال ارسال…' : 'ارسال فاکتور برای مشتری' }}
|
||||
</button>
|
||||
<button type="button" class="admin-btn admin-btn--outline-warning">نیاز به اصلاح</button>
|
||||
<button type="button" class="admin-btn admin-btn--outline-danger">رد سفارش</button>
|
||||
</div>
|
||||
|
||||
<p class="pricing-page__invoice-hint">
|
||||
<template v-if="invoiceSent">
|
||||
فاکتور این سفارش برای مشتری ارسال شده است؛ انتخاب روش تسویه در اختیار اوست.
|
||||
</template>
|
||||
<template v-else-if="canSendInvoice">
|
||||
مبلغ نهایی آماده است. با ارسال فاکتور، مشتری برای انتخاب نقدی یا چک مطلع میشود.
|
||||
</template>
|
||||
<template v-else>
|
||||
ابتدا قیمت واحد اقلام و هزینه حمل را ذخیره کنید تا مبلغ نهایی فاکتور ساخته شود.
|
||||
</template>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<BaseModal :open="showAddModal" title="افزودن محصول" size="lg" @close="closeAddModal">
|
||||
@@ -473,6 +539,13 @@ async function savePricing() {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.pricing-page__invoice-hint {
|
||||
margin: 12px 0 0;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.8;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.pricing-page__line-total {
|
||||
grid-column: 1 / -1;
|
||||
margin: 0;
|
||||
|
||||
@@ -19,6 +19,9 @@ const {
|
||||
settlementLabel,
|
||||
goPricing,
|
||||
goChecks,
|
||||
goCashReceipt,
|
||||
changeStatus,
|
||||
statusSaving,
|
||||
openAddModal,
|
||||
} = useOrdersPageContext()
|
||||
</script>
|
||||
@@ -106,8 +109,11 @@ const {
|
||||
<OrderDetailContent
|
||||
:order="selectedOrder"
|
||||
:settlement-label="settlementLabel"
|
||||
:status-saving="statusSaving"
|
||||
@pricing="goPricing(selectedOrder)"
|
||||
@checks="goChecks(selectedOrder)"
|
||||
@cash-receipt="goCashReceipt(selectedOrder)"
|
||||
@status="changeStatus"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,9 @@ const {
|
||||
settlementLabel,
|
||||
goPricing,
|
||||
goChecks,
|
||||
goCashReceipt,
|
||||
changeStatus,
|
||||
statusSaving,
|
||||
openAddModal,
|
||||
} = useOrdersPageContext()
|
||||
|
||||
@@ -34,6 +37,12 @@ function handleChecks() {
|
||||
closeDetailModal()
|
||||
goChecks(selectedOrder.value)
|
||||
}
|
||||
|
||||
function handleCashReceipt() {
|
||||
if (!selectedOrder.value) return
|
||||
closeDetailModal()
|
||||
goCashReceipt(selectedOrder.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -143,8 +152,11 @@ function handleChecks() {
|
||||
:order="selectedOrder"
|
||||
layout="desktop"
|
||||
:settlement-label="settlementLabel"
|
||||
:status-saving="statusSaving"
|
||||
@pricing="handlePricing"
|
||||
@checks="handleChecks"
|
||||
@cash-receipt="handleCashReceipt"
|
||||
@status="changeStatus"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,19 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import AdminIcon from '@/components/icons/AdminIcon.vue'
|
||||
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||||
import { formatCurrency, formatMeterage, formatNumber } from '@/utils/format'
|
||||
import type { Order } from '@/types'
|
||||
import { RAW_ORDER_STATUS_LABELS, rawStatusLabel } from '@/utils/orderStatus'
|
||||
import type { Order, RawOrderStatus } from '@/types'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
order: Order
|
||||
settlementLabel: (method: string) => string
|
||||
settlementLabel: (method?: string | null) => string
|
||||
layout?: 'desktop' | 'compact'
|
||||
statusSaving?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
pricing: []
|
||||
checks: []
|
||||
cashReceipt: []
|
||||
status: [
|
||||
payload: {
|
||||
orderId: string
|
||||
status: RawOrderStatus
|
||||
note?: string
|
||||
reason?: string
|
||||
force?: boolean
|
||||
},
|
||||
]
|
||||
}>()
|
||||
|
||||
const manualMode = ref(false)
|
||||
const nextStatus = ref<RawOrderStatus | ''>('')
|
||||
const statusNote = ref('')
|
||||
|
||||
const allStatuses = Object.keys(RAW_ORDER_STATUS_LABELS) as RawOrderStatus[]
|
||||
|
||||
const transitions = computed(() => props.order.allowedTransitions ?? [])
|
||||
|
||||
const statusOptions = computed<RawOrderStatus[]>(() =>
|
||||
manualMode.value
|
||||
? allStatuses.filter((s) => s !== props.order.rawStatus)
|
||||
: [...transitions.value],
|
||||
)
|
||||
|
||||
const noteRequired = computed(() => manualMode.value)
|
||||
const canSubmitStatus = computed(
|
||||
() => !!nextStatus.value && (!noteRequired.value || statusNote.value.trim().length > 0),
|
||||
)
|
||||
|
||||
watch([() => props.order.id, manualMode], () => {
|
||||
nextStatus.value = ''
|
||||
statusNote.value = ''
|
||||
})
|
||||
|
||||
function submitStatus() {
|
||||
if (!nextStatus.value || !canSubmitStatus.value) return
|
||||
const note = statusNote.value.trim() || undefined
|
||||
emit('status', {
|
||||
orderId: props.order.id,
|
||||
status: nextStatus.value,
|
||||
note,
|
||||
reason: manualMode.value ? note : undefined,
|
||||
force: manualMode.value || undefined,
|
||||
})
|
||||
nextStatus.value = ''
|
||||
statusNote.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -21,6 +72,7 @@ defineEmits<{
|
||||
<div class="order-detail__id">
|
||||
<strong>{{ order.orderNumber }}</strong>
|
||||
<StatusBadge :status="order.status" />
|
||||
<span class="order-detail__raw-status">{{ rawStatusLabel(order.rawStatus) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="order-detail__section">
|
||||
@@ -59,7 +111,7 @@ defineEmits<{
|
||||
<div class="admin-info-row">
|
||||
<span class="admin-info-row__label">روش تسویه:</span>
|
||||
<span class="admin-info-row__value">
|
||||
{{ settlementLabel(order.settlementMethod) }} — مختص
|
||||
{{ settlementLabel(order.settlementMethod) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="admin-info-row">
|
||||
@@ -114,6 +166,57 @@ defineEmits<{
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="order-detail__section order-detail__status">
|
||||
<div class="order-detail__status-head">
|
||||
<h3 class="order-detail__section-title">تغییر وضعیت</h3>
|
||||
<label class="order-detail__manual">
|
||||
<input v-model="manualMode" type="checkbox" />
|
||||
<span>تغییر دستی</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p v-if="manualMode" class="order-detail__status-hint">
|
||||
این گذر خارج از روال استاندارد است؛ ثبت یادداشت اجباری است و در تاریخچهٔ سفارش ذخیره میشود.
|
||||
</p>
|
||||
<p v-else-if="!statusOptions.length" class="order-detail__status-hint">
|
||||
در این وضعیت گذر خودکاری وجود ندارد. برای تغییر، حالت دستی را فعال کنید.
|
||||
</p>
|
||||
|
||||
<template v-if="statusOptions.length">
|
||||
<div class="admin-form__field">
|
||||
<label for="next-status">وضعیت بعدی</label>
|
||||
<select id="next-status" v-model="nextStatus" class="admin-input">
|
||||
<option value="">انتخاب کنید…</option>
|
||||
<option v-for="status in statusOptions" :key="status" :value="status">
|
||||
{{ rawStatusLabel(status) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label for="status-note">
|
||||
یادداشت{{ noteRequired ? ' (اجباری)' : ' (اختیاری)' }}
|
||||
</label>
|
||||
<input
|
||||
id="status-note"
|
||||
v-model="statusNote"
|
||||
type="text"
|
||||
class="admin-input"
|
||||
placeholder="دلیل یا توضیح این تغییر"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="admin-btn admin-btn--primary admin-btn--sm"
|
||||
:disabled="!canSubmitStatus || statusSaving"
|
||||
@click="submitStatus"
|
||||
>
|
||||
{{ statusSaving ? 'در حال ثبت…' : 'ثبت وضعیت' }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="order-detail__footer">
|
||||
<button type="button" class="admin-btn admin-btn--primary admin-btn--sm" @click="$emit('pricing')">
|
||||
قیمتگذاری سفارش
|
||||
@@ -126,6 +229,14 @@ defineEmits<{
|
||||
>
|
||||
مدیریت چکها
|
||||
</button>
|
||||
<button
|
||||
v-if="order.settlementMethod === 'cash'"
|
||||
type="button"
|
||||
class="admin-btn admin-btn--outline admin-btn--sm"
|
||||
@click="$emit('cashReceipt')"
|
||||
>
|
||||
رسید پرداخت این سفارش
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -178,6 +289,40 @@ defineEmits<{
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.order-detail__raw-status {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.order-detail__status-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.order-detail__manual {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.order-detail__manual input {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
accent-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.order-detail__status-hint {
|
||||
margin: 0 0 12px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.7;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.order-detail__footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,24 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { settingsApi, type UpdateSettingsPayload } from '@/services/api'
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const settings = reactive({
|
||||
const settings = reactive<UpdateSettingsPayload>({
|
||||
maxMeteragePerInvoice: 5000,
|
||||
cashInvoiceValidityHours: 8,
|
||||
smsOrderRegistered: true,
|
||||
smsPricingReady: true,
|
||||
smsPaymentReminder: true,
|
||||
smsCheckReview: true,
|
||||
smsShippingUpdate: true,
|
||||
defaultSettlementCheck: true,
|
||||
defaultSettlementCash: true,
|
||||
supportPhone: '۰۲۱-۸۸۷۷۶۶۵۵',
|
||||
supportEmail: 'support@fabrictrade.ir',
|
||||
supportPhone: '',
|
||||
supportEmail: '',
|
||||
})
|
||||
|
||||
function saveSettings() {
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await settingsApi.get()
|
||||
Object.assign(settings, {
|
||||
maxMeteragePerInvoice: data.maxMeteragePerInvoice,
|
||||
cashInvoiceValidityHours: data.cashInvoiceValidityHours,
|
||||
smsOrderRegistered: data.smsOrderRegistered,
|
||||
smsPricingReady: data.smsPricingReady,
|
||||
smsPaymentReminder: data.smsPaymentReminder,
|
||||
smsCheckReview: data.smsCheckReview,
|
||||
smsShippingUpdate: data.smsShippingUpdate,
|
||||
defaultSettlementCheck: data.defaultSettlementCheck,
|
||||
defaultSettlementCash: data.defaultSettlementCash,
|
||||
supportPhone: data.supportPhone ?? '',
|
||||
supportEmail: data.supportEmail ?? '',
|
||||
})
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : 'دریافت تنظیمات ناموفق بود.')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
async function saveSettings() {
|
||||
if (saving.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
await settingsApi.update({ ...settings })
|
||||
toast.success('تنظیمات با موفقیت ذخیره شد.')
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : 'ذخیره تنظیمات ناموفق بود.')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -29,8 +67,13 @@ function saveSettings() {
|
||||
<h1 class="admin-page-header__title">تنظیمات</h1>
|
||||
<p class="admin-page-header__breadcrumb">سیستم / تنظیمات</p>
|
||||
</div>
|
||||
<button type="button" class="admin-btn admin-btn--primary" @click="saveSettings">
|
||||
ذخیره تنظیمات
|
||||
<button
|
||||
type="button"
|
||||
class="admin-btn admin-btn--primary"
|
||||
:disabled="loading || saving"
|
||||
@click="saveSettings"
|
||||
>
|
||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره تنظیمات' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -80,6 +123,10 @@ function saveSettings() {
|
||||
<input v-model="settings.smsOrderRegistered" type="checkbox" />
|
||||
<span>پیامک ثبت سفارش</span>
|
||||
</label>
|
||||
<label class="settings-page__check">
|
||||
<input v-model="settings.smsPricingReady" type="checkbox" />
|
||||
<span>پیامک صدور فاکتور و آمادهبودن قیمت</span>
|
||||
</label>
|
||||
<label class="settings-page__check">
|
||||
<input v-model="settings.smsPaymentReminder" type="checkbox" />
|
||||
<span>یادآوری پایان اعتبار فاکتور</span>
|
||||
|
||||
Reference in New Issue
Block a user