Files
nademitex_admin/src/views/orders/components/OrderDetailContent.vue
T
2026-09-29 22:00:50 +03:30

593 lines
18 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import AdminIcon from '@/components/icons/AdminIcon.vue'
import AdminSelect from '@/components/ui/AdminSelect.vue'
import StatusBadge from '@/components/ui/StatusBadge.vue'
import { formatCurrency, formatDateTime, formatMeterage, formatNumber } from '@/utils/format'
import { CASH_PAYMENT_STATUS_LABELS, CUSTOMER_TYPE_LABELS } from '@/utils/adminLabels'
import AdminImageLightbox from '@/components/ui/AdminImageLightbox.vue'
import { ORDER_STATUS_META, canFinalApproveOrder, canPriceOrder, rawStatusLabel } from '@/utils/orderStatus'
import type { CustomerType, Order, RawOrderStatus } from '@/types'
const props = withDefaults(
defineProps<{
order: Order
settlementLabel: (method?: string | null) => string
layout?: 'desktop' | 'compact'
statusSaving?: boolean
}>(),
{ layout: 'compact' },
)
const emit = defineEmits<{
pricing: []
checks: []
cashReceipt: []
finalApprove: []
status: [
payload: {
orderId: string
status: RawOrderStatus
note?: string
},
]
}>()
const nextStatus = ref<RawOrderStatus | ''>('')
const statusNote = ref('')
const receiptPreviewOpen = ref(false)
const cashReceipt = computed(() => props.order.cashReceipt ?? null)
const transitions = computed(() => props.order.allowedTransitions ?? [])
const statusOptions = computed<RawOrderStatus[]>(() => [...transitions.value])
const statusSelectOptions = computed(() => [
{ value: '', label: 'انتخاب کنید…' },
...statusOptions.value.map((status) => ({
value: status,
label: rawStatusLabel(status),
})),
])
const canSubmitStatus = computed(() => !!nextStatus.value)
const useTable = computed(() => props.layout === 'desktop')
const badgeStatusLabel = computed(
() => ORDER_STATUS_META[props.order.status]?.label ?? props.order.status,
)
/** Avoid duplicating the same Persian label under the badge. */
const distinctRawStatusLabel = computed(() => {
const raw = rawStatusLabel(props.order.rawStatus)
if (!raw || raw === badgeStatusLabel.value) return ''
return raw
})
const customerTypeLabel = computed(() => {
const type = props.order.customerType as CustomerType | undefined
if (!type) return '—'
return CUSTOMER_TYPE_LABELS[type] ?? props.order.customerType ?? '—'
})
const showPricingAction = computed(() => canPriceOrder(props.order))
const showFinalApprove = computed(() => canFinalApproveOrder(props.order))
const showChecksAction = computed(
() => props.order.settlementMethod === 'check' && !showPricingAction.value,
)
const showCashAction = computed(
() =>
props.order.settlementMethod === 'cash' &&
!showPricingAction.value &&
props.order.rawStatus !== 'cash_confirmed',
)
/** Primary CTA follows the current workflow stage. */
const primaryAction = computed<'pricing' | 'final' | 'checks' | 'cash' | null>(() => {
if (showPricingAction.value) return 'pricing'
if (showFinalApprove.value) return 'final'
if (showChecksAction.value) return 'checks'
if (showCashAction.value) return 'cash'
return null
})
watch(
() => props.order.id,
() => {
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,
})
nextStatus.value = ''
statusNote.value = ''
}
function onCashReceiptAction() {
if (cashReceipt.value?.receiptImage) {
receiptPreviewOpen.value = true
return
}
emit('cashReceipt')
}
</script>
<template>
<div class="order-detail" :class="`order-detail--${layout}`">
<header class="order-detail__hero">
<div class="order-detail__hero-text">
<div class="order-detail__id">
<strong>{{ order.orderNumber }}</strong>
<StatusBadge :status="order.status" />
</div>
<p v-if="distinctRawStatusLabel" class="order-detail__raw-status">
{{ distinctRawStatusLabel }}
</p>
</div>
<div class="order-detail__actions no-print">
<button
v-if="showPricingAction"
type="button"
class="admin-btn admin-btn--sm"
:class="primaryAction === 'pricing' ? 'admin-btn--primary' : 'admin-btn--outline'"
@click="emit('pricing')"
>
قیمت‌گذاری سفارش
</button>
<button
v-if="showChecksAction"
type="button"
class="admin-btn admin-btn--sm"
:class="primaryAction === 'checks' ? 'admin-btn--primary' : 'admin-btn--outline'"
@click="emit('checks')"
>
مدیریت چک‌ها
</button>
<button
v-if="showCashAction"
type="button"
class="admin-btn admin-btn--sm"
:class="primaryAction === 'cash' ? 'admin-btn--primary' : 'admin-btn--outline'"
@click="onCashReceiptAction"
>
{{ cashReceipt?.receiptImage ? 'مشاهده رسید' : 'رسید پرداخت' }}
</button>
<button
v-if="showFinalApprove"
type="button"
class="admin-btn admin-btn--sm"
:class="primaryAction === 'final' ? 'admin-btn--primary' : 'admin-btn--outline'"
:disabled="statusSaving"
@click="emit('finalApprove')"
>
تأیید نهایی
</button>
</div>
</header>
<div class="order-detail__panels">
<section class="order-detail__section">
<h3 class="order-detail__section-title">اطلاعات مشتری</h3>
<div class="order-detail__info">
<div class="order-detail__info-row">
<AdminIcon name="building" :size="16" />
<span class="order-detail__info-label">مشتری</span>
<span class="order-detail__info-value">{{ order.customerName }}</span>
</div>
<div class="order-detail__info-row">
<AdminIcon name="phone" :size="16" />
<span class="order-detail__info-label">تلفن</span>
<span class="order-detail__info-value order-detail__info-value--phone" dir="ltr">{{ order.customerPhone ?? '—' }}</span>
</div>
<div class="order-detail__info-row">
<AdminIcon name="id-card" :size="16" />
<span class="order-detail__info-label">شناسه ملی</span>
<span class="order-detail__info-value">{{ order.customerNationalId ?? '—' }}</span>
</div>
<div class="order-detail__info-row">
<AdminIcon name="user" :size="16" />
<span class="order-detail__info-label">نوع مشتری</span>
<span class="order-detail__info-value">{{ customerTypeLabel }}</span>
</div>
</div>
</section>
<section class="order-detail__section">
<h3 class="order-detail__section-title">اطلاعات سفارش</h3>
<div class="order-detail__info">
<div class="order-detail__info-row">
<span class="order-detail__info-label">شرکت</span>
<span class="order-detail__info-value">{{ order.companyName }}</span>
</div>
<div class="order-detail__info-row">
<span class="order-detail__info-label">روش تسویه</span>
<span class="order-detail__info-value">{{ settlementLabel(order.settlementMethod) }}</span>
</div>
<div class="order-detail__info-row">
<span class="order-detail__info-label">متراژ کل</span>
<span class="order-detail__info-value">{{ formatMeterage(order.meterage) }}</span>
</div>
<div class="order-detail__info-row">
<span class="order-detail__info-label">مبلغ کل</span>
<span class="order-detail__info-value">
{{ order.totalAmount != null ? formatCurrency(order.totalAmount) : 'پس از قیمت‌گذاری' }}
</span>
</div>
</div>
</section>
</div>
<section v-if="order.items?.length" class="order-detail__section">
<h3 class="order-detail__section-title">محصولات سفارش</h3>
<div v-if="useTable" class="admin-table-wrap order-detail__table">
<table class="admin-table admin-table--compact">
<thead>
<tr>
<th>شرکت</th>
<th>کالیته</th>
<th>محصول</th>
<th>رنگ</th>
<th>متراژ</th>
<th>قیمت</th>
<th>مبلغ</th>
</tr>
</thead>
<tbody>
<tr v-for="item in order.items" :key="item.id">
<td>{{ item.companyName || order.companyName }}</td>
<td>{{ item.catalogCode }}</td>
<td>{{ item.productName }}</td>
<td>{{ item.color }}</td>
<td>{{ formatNumber(item.confirmedMeterage ?? item.requestedMeterage) }}</td>
<td>{{ item.supplyPrice != null ? formatCurrency(item.supplyPrice) : '—' }}</td>
<td>{{ item.totalAmount != null ? formatCurrency(item.totalAmount) : '—' }}</td>
</tr>
</tbody>
</table>
</div>
<ul v-else class="order-detail__items">
<li v-for="item in order.items" :key="item.id" class="order-detail__item">
<div class="order-detail__item-head">
<strong>{{ item.productName }}</strong>
<span>{{ item.catalogCode }}</span>
</div>
<div class="order-detail__item-meta">
<span v-if="item.companyName">{{ item.companyName }}</span>
<span>{{ item.color }}</span>
<span>{{ formatMeterage(item.confirmedMeterage ?? item.requestedMeterage) }}</span>
<span v-if="item.supplyPrice != null">{{ formatCurrency(item.supplyPrice) }}</span>
<span v-if="item.totalAmount != null" class="order-detail__item-total">
{{ formatCurrency(item.totalAmount) }}
</span>
</div>
</li>
</ul>
</section>
<section v-if="cashReceipt" class="order-detail__section">
<h3 class="order-detail__section-title">رسید پرداخت نقدی</h3>
<div class="order-detail__info">
<div class="order-detail__info-row">
<span class="order-detail__info-label">وضعیت</span>
<span class="order-detail__info-value">
{{ CASH_PAYMENT_STATUS_LABELS[cashReceipt.status] }}
</span>
</div>
<div class="order-detail__info-row">
<span class="order-detail__info-label">مبلغ واریزی</span>
<span class="order-detail__info-value">
{{
cashReceipt.paidAmount != null
? formatCurrency(cashReceipt.paidAmount)
: formatCurrency(cashReceipt.amount)
}}
</span>
</div>
<div v-if="cashReceipt.transferDate" class="order-detail__info-row">
<span class="order-detail__info-label">تاریخ واریز</span>
<span class="order-detail__info-value">
{{ formatDateTime(cashReceipt.transferDate) }}
<template v-if="cashReceipt.transferTime"> · {{ cashReceipt.transferTime }}</template>
</span>
</div>
<div v-if="cashReceipt.trackingNumber || cashReceipt.trackingCode" class="order-detail__info-row">
<span class="order-detail__info-label">پیگیری</span>
<span class="order-detail__info-value" dir="ltr">
{{ cashReceipt.trackingNumber || cashReceipt.trackingCode }}
</span>
</div>
<div v-if="cashReceipt.notes" class="order-detail__info-row">
<span class="order-detail__info-label">یادداشت</span>
<span class="order-detail__info-value">{{ cashReceipt.notes }}</span>
</div>
</div>
<div v-if="cashReceipt.receiptImage" class="order-detail__receipt">
<button
type="button"
class="order-detail__receipt-thumb"
@click="receiptPreviewOpen = true"
>
<img :src="cashReceipt.receiptImage" alt="تصویر رسید پرداخت" />
</button>
<button
type="button"
class="admin-btn admin-btn--outline admin-btn--sm"
@click="receiptPreviewOpen = true"
>
مشاهده تصویر رسید
</button>
</div>
<p v-else class="order-detail__status-hint">تصویر رسید هنوز آپلود نشده است.</p>
</section>
<AdminImageLightbox
v-if="cashReceipt?.receiptImage"
v-model="receiptPreviewOpen"
:src="cashReceipt.receiptImage"
alt="تصویر رسید پرداخت"
/>
<section class="order-detail__section order-detail__status no-print">
<h3 class="order-detail__section-title">تغییر وضعیت</h3>
<p v-if="!statusOptions.length" class="order-detail__status-hint">
در این مرحله گذر مجازی از این بخش نیست؛ از اقدام‌های مرحله‌ای بالا (قیمت‌گذاری، چک، رسید نقدی و …) استفاده کنید.
</p>
<div v-if="statusOptions.length" class="order-detail__status-form">
<div class="admin-form__field">
<label>وضعیت بعدی</label>
<AdminSelect
v-model="nextStatus"
block
aria-label="وضعیت بعدی"
placeholder="انتخاب کنید…"
:options="statusSelectOptions"
/>
</div>
<div class="admin-form__field">
<label for="status-note">یادداشت (اختیاری)</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>
</div>
</section>
</div>
</template>
<style scoped>
.order-detail {
display: flex;
flex-direction: column;
gap: 16px;
}
.order-detail__hero {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-elevated, var(--color-bg));
}
.order-detail__id {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
margin-bottom: 0;
font-size: 1rem;
}
.order-detail__id strong {
line-height: 1.3;
}
.order-detail__raw-status {
margin: 0;
font-size: 0.75rem;
color: var(--color-text-muted);
}
.order-detail__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.order-detail__actions .admin-btn {
min-height: 40px;
}
.order-detail__panels {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.order-detail__section {
margin: 0;
padding: 14px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface);
}
.order-detail__section-title {
margin: 0 0 12px;
font-size: 0.8125rem;
font-weight: 700;
color: var(--color-text);
}
.order-detail__info {
display: grid;
gap: 10px;
}
.order-detail__info-row {
display: grid;
grid-template-columns: auto minmax(72px, auto) 1fr;
align-items: center;
gap: 8px;
font-size: 0.8125rem;
}
.order-detail__info-row:not(:has(svg)) {
grid-template-columns: minmax(72px, auto) 1fr;
}
.order-detail__info-label {
color: var(--color-text-muted);
}
.order-detail__info-value {
font-weight: 600;
color: var(--color-text);
word-break: break-word;
}
.order-detail__info-value--phone {
text-align: right;
justify-self: stretch;
}
.order-detail__table {
overflow-x: auto;
}
.order-detail__items {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 8px;
}
.order-detail__item {
padding: 12px;
border-radius: var(--radius-sm);
background: var(--color-elevated, var(--color-bg));
border: 1px solid var(--color-border);
}
.order-detail__item-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
margin-bottom: 6px;
font-size: 0.875rem;
}
.order-detail__item-head span {
font-size: 0.75rem;
color: var(--color-text-muted);
}
.order-detail__item-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
font-size: 0.75rem;
color: var(--color-text-muted);
}
.order-detail__item-total {
margin-inline-start: auto;
font-weight: 700;
color: var(--color-text);
}
.order-detail__receipt {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-top: 12px;
}
.order-detail__receipt-thumb {
padding: 0;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-bg);
cursor: pointer;
overflow: hidden;
line-height: 0;
}
.order-detail__receipt-thumb img {
display: block;
width: 120px;
height: 120px;
object-fit: cover;
}
.order-detail__status .order-detail__section-title {
margin: 0 0 8px;
}
.order-detail__status-hint {
margin: 0 0 12px;
font-size: 0.75rem;
line-height: 1.7;
color: var(--color-text-muted);
}
.order-detail__status-form {
display: grid;
gap: 12px;
}
@media (max-width: 639px) {
.order-detail__panels {
grid-template-columns: 1fr;
}
.order-detail__hero {
flex-direction: column;
}
.order-detail__actions {
width: 100%;
}
.order-detail__actions .admin-btn {
flex: 1;
}
}
</style>