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