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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user