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:
2026-08-09 12:00:18 +03:30
co-authored by Cursor
parent f5ff02ada4
commit 265e5e0e30
11 changed files with 507 additions and 31 deletions
+13 -2
View File
@@ -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) {
+42 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+41 -2
View File
@@ -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: 'تحویل شده' },
+3 -1
View File
@@ -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)
+78 -5
View File
@@ -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;
+55 -8
View File
@@ -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() {
toast.success('تنظیمات با موفقیت ذخیره شد.')
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>