Normalize inbox /orders/:id links, open related admin pages on click, and keep Jalali dates consistent. Co-authored-by: Cursor <cursoragent@cursor.com>
337 lines
11 KiB
Vue
337 lines
11 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import AdminIcon from '@/components/icons/AdminIcon.vue'
|
||
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||
import { checksApi, mapAdminOrder, ordersApi, type ApiOrder } from '@/services/api'
|
||
import { ApiError } from '@/services/http'
|
||
import { formatCurrency, formatDate } from '@/utils/format'
|
||
import { useToast } from '@/composables/useToast'
|
||
import { confirm } from '@/composables/useConfirm'
|
||
import type { Check } from '@/types'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const toast = useToast()
|
||
|
||
const activeTab = ref<'info' | 'images' | 'history'>('info')
|
||
const order = ref<ApiOrder | null>(null)
|
||
const orderChecks = ref<Check[]>([])
|
||
const busyId = ref<string | null>(null)
|
||
|
||
const displayOrder = computed(() =>
|
||
order.value ? mapAdminOrder(order.value) : null,
|
||
)
|
||
|
||
const totalCheckAmount = computed(() =>
|
||
orderChecks.value.reduce((sum, c) => sum + c.amount, 0),
|
||
)
|
||
|
||
async function loadData() {
|
||
const orderId = String(route.params.id)
|
||
try {
|
||
const [orderData, checksData] = await Promise.all([
|
||
ordersApi.get(orderId),
|
||
checksApi.listByOrder(orderId),
|
||
])
|
||
order.value = orderData
|
||
orderChecks.value = checksData
|
||
} catch (err) {
|
||
toast.error(err instanceof ApiError ? err.message : 'بارگذاری چکها انجام نشد.')
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
void loadData()
|
||
})
|
||
|
||
function goBack() {
|
||
router.push('/orders')
|
||
}
|
||
|
||
function checkBadgeStatus(status: Check['status']) {
|
||
if (status === 'approved') return 'final_approval'
|
||
if (status === 'rejected') return 'needs_correction'
|
||
return 'pending_check_review'
|
||
}
|
||
|
||
async function updateCheckStatus(check: Check, status: Check['status']) {
|
||
if (status !== 'approved' && status !== 'rejected') return
|
||
|
||
const amountLabel = formatCurrency(check.amount)
|
||
const ok = await confirm(
|
||
status === 'approved'
|
||
? {
|
||
title: 'تأیید چک',
|
||
message: `آیا از تأیید چک ردیف ${check.row} به مبلغ ${amountLabel} مطمئن هستید؟`,
|
||
tone: 'primary',
|
||
confirmLabel: 'تأیید چک',
|
||
cancelLabel: 'انصراف',
|
||
}
|
||
: {
|
||
title: 'رد چک',
|
||
message: `آیا از رد چک ردیف ${check.row} به مبلغ ${amountLabel} مطمئن هستید؟`,
|
||
tone: 'danger',
|
||
confirmLabel: 'رد چک',
|
||
cancelLabel: 'انصراف',
|
||
},
|
||
)
|
||
if (!ok) return
|
||
|
||
busyId.value = check.id
|
||
try {
|
||
const updated = await checksApi.updateStatus(check.id, { status })
|
||
const index = orderChecks.value.findIndex((c) => c.id === check.id)
|
||
if (index !== -1) orderChecks.value[index] = updated
|
||
toast.success(status === 'approved' ? 'چک تأیید شد.' : 'چک رد شد.')
|
||
} catch (err) {
|
||
toast.error(err instanceof ApiError ? err.message : 'بهروزرسانی وضعیت چک انجام نشد.')
|
||
} finally {
|
||
busyId.value = null
|
||
}
|
||
}
|
||
|
||
async function approveAll() {
|
||
const pending = orderChecks.value.filter((c) => c.status === 'pending_review')
|
||
if (!pending.length) {
|
||
toast.error('چکی برای تأیید وجود ندارد.')
|
||
return
|
||
}
|
||
|
||
const ok = await confirm({
|
||
title: 'تأیید همه چکها',
|
||
message: `آیا از تأیید ${pending.length} چک در انتظار بررسی مطمئن هستید؟`,
|
||
tone: 'primary',
|
||
confirmLabel: 'تأیید همه',
|
||
cancelLabel: 'انصراف',
|
||
})
|
||
if (!ok) return
|
||
|
||
for (const check of pending) {
|
||
busyId.value = check.id
|
||
try {
|
||
const updated = await checksApi.updateStatus(check.id, { status: 'approved' })
|
||
const index = orderChecks.value.findIndex((c) => c.id === check.id)
|
||
if (index !== -1) orderChecks.value[index] = updated
|
||
} catch (err) {
|
||
toast.error(err instanceof ApiError ? err.message : 'بهروزرسانی وضعیت چک انجام نشد.')
|
||
busyId.value = null
|
||
return
|
||
}
|
||
}
|
||
busyId.value = null
|
||
toast.success('همه چکهای در انتظار تأیید شدند.')
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="checks-page">
|
||
<div class="admin-page-header">
|
||
<div>
|
||
<h1 class="admin-page-header__title">مدیریت چکها</h1>
|
||
<p class="admin-page-header__breadcrumb">سفارشها / مدیریت سفارش</p>
|
||
</div>
|
||
<div v-if="displayOrder" class="checks-page__order-meta">
|
||
<strong>{{ displayOrder.orderNumber }}</strong>
|
||
<StatusBadge :status="displayOrder.status" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="admin-tabs">
|
||
<button
|
||
type="button"
|
||
class="admin-tab"
|
||
:class="{ active: activeTab === 'info' }"
|
||
@click="activeTab = 'info'"
|
||
>
|
||
اطلاعات چکها ({{ orderChecks.length }})
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="admin-tab"
|
||
:class="{ active: activeTab === 'images' }"
|
||
@click="activeTab = 'images'"
|
||
>
|
||
تصاویر چکها
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="admin-tab"
|
||
:class="{ active: activeTab === 'history' }"
|
||
@click="activeTab = 'history'"
|
||
>
|
||
تاریخچه بررسی
|
||
</button>
|
||
</div>
|
||
|
||
<template v-if="activeTab === 'info'">
|
||
<div class="admin-summary-grid">
|
||
<div class="admin-summary-item">
|
||
<p class="admin-summary-item__label">تعداد چکها</p>
|
||
<p class="admin-summary-item__value">{{ orderChecks.length }} فقره</p>
|
||
</div>
|
||
<div class="admin-summary-item">
|
||
<p class="admin-summary-item__label">راس چکها</p>
|
||
<p class="admin-summary-item__value">
|
||
<AdminIcon name="check" :size="14" />
|
||
{{ order?.checkRassDays ?? '—' }} روز
|
||
</p>
|
||
<p v-if="order?.checkRassDate" class="checks-page__rass-date">
|
||
تاریخ راس: {{ formatDate(order.checkRassDate) }}
|
||
</p>
|
||
</div>
|
||
<div class="admin-summary-item">
|
||
<p class="admin-summary-item__label">مجموع چکها</p>
|
||
<p class="admin-summary-item__value">{{ formatCurrency(totalCheckAmount) }}</p>
|
||
</div>
|
||
<div class="admin-summary-item">
|
||
<p class="admin-summary-item__label">مبلغ کل سفارش</p>
|
||
<p class="admin-summary-item__value">
|
||
{{ displayOrder?.totalAmount ? formatCurrency(displayOrder.totalAmount) : '—' }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="checks-page__table admin-card desktop-only">
|
||
<div class="admin-table-wrap">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>ردیف</th>
|
||
<th>مبلغ (تومان)</th>
|
||
<th>بانک</th>
|
||
<th>شناسه صیادی</th>
|
||
<th>تاریخ سررسید</th>
|
||
<th>وضعیت</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="check in orderChecks" :key="check.id">
|
||
<td>{{ check.row }}</td>
|
||
<td>{{ formatCurrency(check.amount) }}</td>
|
||
<td>{{ check.bankName }}</td>
|
||
<td>{{ check.sayadId }}</td>
|
||
<td>{{ formatDate(check.dueDate) }}</td>
|
||
<td><StatusBadge :status="checkBadgeStatus(check.status)" /></td>
|
||
<td>
|
||
<div v-if="check.status === 'pending_review'" class="admin-action-group">
|
||
<button
|
||
type="button"
|
||
class="admin-btn admin-btn--outline-success admin-btn--xs"
|
||
:disabled="busyId === check.id"
|
||
@click="updateCheckStatus(check, 'approved')"
|
||
>
|
||
تأیید
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="admin-btn admin-btn--outline-danger admin-btn--xs"
|
||
:disabled="busyId === check.id"
|
||
@click="updateCheckStatus(check, 'rejected')"
|
||
>
|
||
رد
|
||
</button>
|
||
</div>
|
||
<span v-else>—</span>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="compact-only admin-card-list">
|
||
<article v-for="check in orderChecks" :key="check.id" class="admin-card-item">
|
||
<div class="admin-card-item__header">
|
||
<p class="admin-card-item__title">چک {{ check.row }} — {{ formatCurrency(check.amount) }}</p>
|
||
<StatusBadge :status="checkBadgeStatus(check.status)" />
|
||
</div>
|
||
<div class="admin-card-item__meta">
|
||
<span>بانک {{ check.bankName }}</span>
|
||
<span>سررسید {{ formatDate(check.dueDate) }}</span>
|
||
<span dir="ltr">{{ check.sayadId }}</span>
|
||
</div>
|
||
<div v-if="check.status === 'pending_review'" class="admin-card-item__actions">
|
||
<button
|
||
type="button"
|
||
class="admin-btn admin-btn--outline-success admin-btn--xs"
|
||
:disabled="busyId === check.id"
|
||
@click="updateCheckStatus(check, 'approved')"
|
||
>
|
||
تأیید
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="admin-btn admin-btn--outline-danger admin-btn--xs"
|
||
:disabled="busyId === check.id"
|
||
@click="updateCheckStatus(check, 'rejected')"
|
||
>
|
||
رد
|
||
</button>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
|
||
<div class="checks-page__footer">
|
||
<p class="checks-page__note">
|
||
راس چکها بر اساس تاریخ ثبت سفارش محاسبه شده است.
|
||
</p>
|
||
<div class="admin-action-group">
|
||
<button type="button" class="admin-btn admin-btn--success" @click="approveAll">
|
||
تأیید همه چکها
|
||
</button>
|
||
<button type="button" class="admin-btn admin-btn--outline">
|
||
<AdminIcon name="print" :size="16" />
|
||
چاپ لیستها
|
||
</button>
|
||
<button type="button" class="admin-btn admin-btn--outline admin-btn--sm" @click="goBack">
|
||
بازگشت
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div v-else class="admin-empty-placeholder admin-card">
|
||
{{ activeTab === 'images' ? 'تصاویر چکها' : 'تاریخچه بررسی' }} — بهزودی
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.checks-page__order-meta {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 6px;
|
||
font-size: 0.9375rem;
|
||
}
|
||
|
||
.checks-page__rass-date {
|
||
margin: 4px 0 0;
|
||
font-size: 0.75rem;
|
||
color: var(--color-text-muted);
|
||
font-weight: 400;
|
||
}
|
||
|
||
.checks-page__table {
|
||
margin-top: 16px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.checks-page__footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
margin-top: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.checks-page__note {
|
||
margin: 0;
|
||
font-size: 0.8125rem;
|
||
color: var(--color-text-muted);
|
||
}
|
||
</style>
|