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
+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;