Files
tehran_taj/apps/user-app/src/views/user/OrdersView.vue
T

283 lines
10 KiB
Vue
Raw Normal View History

2026-09-09 06:46:27 +03:30
<!--
Ported from legacy henza_pwa/src/views/user/src/components/Orders.vue.
Adapted:
- Status filter tabs use our real OrderStatus enum values directly (see
utils/orderStatus.ts) rather than legacy's own inconsistent tab labels.
- Fetches real order history via the already-existing GET
/customers/:id/orders endpoint, filtered client-side by tab (that
endpoint has no server-side status filter — small enough dataset that
client-side filtering is reasonable, and apps/api stays untouched).
- Search matches against `orderNumber` (our real user-facing identifier)
instead of legacy's numeric `id`.
DROPPED: "دریافت فاکتور" (get invoice) button — legacy's handler opened a
hardcoded link to the real legacy production domain
(https://henzagold.com/oapi/...). Not just unimplemented — an actual
external production URL that must never be carried into this port. No
invoice-generation endpoint exists in the new API either.
-->
<template>
<div class="fixed top-0 z-10 w-full bg-white">
<AppHeader>
<div class="flex justify-between items-center">
<Icon icon="vuesax-linear:arrow-right-1" class="cursor-pointer text-white" size="25" @click="router.push({ name: 'home' })" />
<h3 class="text-white bold mb-0">سفارشات</h3>
<div></div>
</div>
</AppHeader>
<div class="pt-3 px-2 pb-3 mb-3 border-b-solid border-[#E5E7EB] w-full">
<SearchInput @search="readSearch" place-holder="جستجو" />
</div>
</div>
<div class="h-36"></div>
<container class="bg-[#F9FAFB] px-2 orders">
<Tabs v-model:activeKey="activeTab" style="padding-right: 0.2rem !important; font-weight: bold">
<TabPane v-for="item in tabs" :key="item.key" :tab="item.title" force-render>
<Spinner :spinning="loading" tip="درحال دریافت اطلاعات" cls="overflow-y-scroll no-scrollbar">
<OrderCard @view="openModal" v-for="item in filteredOrders" :key="item.id" :value="item" />
<EmptyState v-if="filteredOrders.length === 0" :image="ordersEmpty" />
</Spinner>
</TabPane>
</Tabs>
<Modal v-model:visible="viewData.model" :mask="false" :footer="false" :closable="false" class="henza-custom-modal" v-if="viewData.item">
<div class="flex justify-between items-center p-4">
<Icon icon="vuesax-linear:arrow-right-1" class="cursor-pointer" size="25" @click="viewData.model = false" />
<h3 class="font-semibold mb-0">جزئیات سفارش</h3>
<div></div>
</div>
<container>
<div class="overflow-y-auto h-screen">
<div class="flex justify-between items-center pt-4">
<span class="font-semibold text-[14px]">اطلاعات سفارش</span>
</div>
<div class="responsive-steps">
<div class="steps-container">
<div v-for="(step, index) in ORDER_STEPS" :key="index" class="step" :class="{ active: currentStep === index }">
<div class="step-circle">{{ index + 1 }}</div>
<div class="step-title">{{ step }}</div>
<div v-if="index !== 0" class="step-line"></div>
</div>
</div>
</div>
<div class="grid grid-cols-2 border-[1.5px] border-[#E5E7EB] border-solid mt-4 rounded-[6px]">
<div class="grid col-span-1 p-4 text-[#9CA3AF] text-[12px] font-medium">
<div>شماره سفارش :</div>
<div class="pt-4">وضعیت سفارش :</div>
<div class="pt-4">تعداد کالاها :</div>
<div class="pt-4">وزن سفارش :</div>
<div class="pt-4">تاریخ سفارش :</div>
</div>
<div class="grid col-span-1 text-left p-4">
<div class="font-bold text-[18px] text-[#23335B]">{{ viewData.item.orderNumber }}</div>
<div class="pt-4 text-center flex justify-end">
<div class="rounded-[5px] py-[4px] px-[12px] text-[14px] font-semibold w-fit" :style="{ background: modalColor?.background, color: modalColor?.font }">
{{ modalStatusLabel }}
</div>
</div>
<div class="pt-4">
<span class="text-[16px] font-bold pl-2 text-black">{{ viewData.item.items.length }}</span>
<span class="text-[14px] font-medium">آیتم</span>
</div>
<div class="pt-4 font-medium">
<span class="text-[16px] font-bold pl-1 text-black">{{ parseFloat(viewData.item.totalGoldWeightGrams).toFixed(2) }}</span>
<span class="pr-2">گرم</span>
</div>
<div class="pt-4 font-semibold text-[#1E2229]">{{ jdate(viewData.item.createdAt) }}</div>
</div>
</div>
<div>
<div class="font-semibold mt-4">آیتم های سفارش</div>
<div class="w-full max-w-screen-md m-auto h-full flex flex-col">
<div class="flex-1 py-[10px] pb-[70px]">
<OrderItemCard v-for="item in viewData.item.items" :key="item.id" :value="item" class="border-[#E5E7EB] border-[1.5px] border-solid text-primary bg-[#F9F9F9] rounded-[6px] mb-4" />
</div>
</div>
</div>
</div>
</container>
</Modal>
</container>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Modal, TabPane, Tabs } from 'ant-design-vue'
import ordersEmpty from '../../assets/img/empty/ordersEmpty.svg'
import Icon from '../../components/icon/Icon.vue'
import AppHeader from '../../components/AppHeader.vue'
import Spinner from '../../components/Spinner.vue'
import EmptyState from '../../components/EmptyState.vue'
import SearchInput from '../../components/search/SearchInput.vue'
import OrderCard from '../../components/product/OrderCard.vue'
import OrderItemCard from '../../components/product/OrderItemCard.vue'
import { ORDER_STATUS_COLORS, ORDER_STATUS_LABELS, ORDER_STEPS, orderStepIndex } from '../../utils/orderStatus'
import { jdate } from '../../utils/filters'
import { useSessionStore } from '../../stores/session'
import { api, type OrderDetail } from '../../api/client'
const router = useRouter()
const session = useSessionStore()
const tabs = [
{ title: 'همه', key: 'ALL' },
{ title: ORDER_STATUS_LABELS.SUSPENDED, key: 'SUSPENDED' },
{ title: ORDER_STATUS_LABELS.PLACED, key: 'PLACED' },
{ title: ORDER_STATUS_LABELS.ADMIN_APPROVED, key: 'ADMIN_APPROVED' },
{ title: ORDER_STATUS_LABELS.SOURCING, key: 'SOURCING' },
{ title: ORDER_STATUS_LABELS.PREPARING, key: 'PREPARING' },
{ title: ORDER_STATUS_LABELS.DELIVERED_TO_AGENT, key: 'DELIVERED_TO_AGENT' },
{ title: ORDER_STATUS_LABELS.DELIVERED_TO_CUSTOMER, key: 'DELIVERED_TO_CUSTOMER' },
{ title: ORDER_STATUS_LABELS.CANCELLED, key: 'CANCELLED' },
]
const activeTab = ref('ALL')
const loading = ref(false)
const allOrders = ref<OrderDetail[]>([])
const searchTerm = ref<string | undefined>(undefined)
const filteredOrders = computed(() => {
let list = allOrders.value
if (activeTab.value !== 'ALL') list = list.filter((o) => o.status === activeTab.value)
if (searchTerm.value) list = list.filter((o) => o.orderNumber.includes(searchTerm.value!))
return list
})
function readSearch(value: string | undefined) {
searchTerm.value = value
}
async function loadOrders() {
if (!session.customerId) return
loading.value = true
try {
allOrders.value = await api.getCustomerOrders(session.customerId)
} finally {
loading.value = false
}
}
const viewData = reactive<{ model: boolean; item: OrderDetail | null }>({ model: false, item: null })
const modalStatusLabel = computed(() => (viewData.item ? ORDER_STATUS_LABELS[viewData.item.status] ?? viewData.item.status : ''))
const modalColor = computed(() => (viewData.item ? ORDER_STATUS_COLORS[viewData.item.status] : undefined))
const currentStep = computed(() => (viewData.item ? orderStepIndex(viewData.item.status) : 0))
function openModal(item: OrderDetail) {
viewData.item = item
viewData.model = true
}
onMounted(loadOrders)
</script>
<style lang="less">
.henza-custom-modal,
.henza-custom-modal .ant-modal-body,
.henza-custom-modal .ant-modal-content {
border-radius: 0;
width: 100vw !important;
max-width: 100vw;
margin: 0;
height: 100vh;
top: 0;
padding: 0;
direction: rtl;
}
.orders {
min-height: calc(100vh - 150px);
height: fit-content;
}
.orders .ant-tabs > .ant-tabs-nav,
.ant-tabs > div > .ant-tabs-nav {
background-color: white;
position: fixed;
z-index: 10;
overflow-x: auto;
width: 100%;
max-width: 768px;
margin: auto;
left: 0;
right: 0;
padding-right: 10px;
padding-left: 10px;
border-bottom-style: solid;
border-bottom-color: #e5e7eb;
border-bottom-width: 1px;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}
.responsive-steps {
width: 100%;
padding: 10px;
.steps-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
text-align: center;
position: relative;
.step-circle {
width: 30px;
height: 30px;
border-radius: 50%;
background-color: #e5e7eb;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: #9ca3af;
z-index: 1;
}
.step-title {
margin-top: 6px;
font-size: 12px;
white-space: normal;
word-break: break-word;
}
.step-line {
position: absolute;
top: 15px;
left: 50%;
width: 100%;
height: 2px;
background-color: #e5e7eb;
z-index: 0;
transform: translateX(0);
}
&.active .step-circle {
background-color: @primary-color;
color: white;
}
}
}
@media (max-width: 600px) {
.step-title {
font-size: 11px;
}
.step-circle {
width: 24px;
height: 24px;
font-size: 10px;
}
}
</style>