283 lines
10 KiB
Vue
283 lines
10 KiB
Vue
<!--
|
|
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>
|