Polish order tracking layout and theme-safe desktop UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-09 16:14:14 +02:00
co-authored by Cursor
parent 5f60111321
commit 3a822b6882
8 changed files with 275 additions and 227 deletions
+10 -4
View File
@@ -39,6 +39,7 @@
.desktop-two-col__main {
min-width: 0;
overflow: hidden;
}
.desktop-summary-panel {
@@ -87,17 +88,21 @@
border-radius: var(--radius-md);
box-shadow: var(--shadow-card);
overflow: hidden;
min-width: 0;
width: 100%;
}
.desktop-table-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
}
.desktop-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
min-width: 640px;
min-width: 560px;
}
.desktop-table th {
@@ -106,16 +111,17 @@
font-size: 0.8125rem;
font-weight: 600;
color: var(--color-text-muted);
background: #f8fafc;
background: var(--color-bg);
border-bottom: 1px solid var(--color-border);
white-space: nowrap;
}
.desktop-table td {
padding: 14px 16px;
border-bottom: 1px solid #f1f5f9;
border-bottom: 1px solid var(--color-border);
color: var(--color-text);
vertical-align: middle;
background: var(--color-surface);
}
.desktop-table tbody tr:last-child td {
@@ -123,7 +129,7 @@
}
.desktop-table tbody tr:hover td {
background: #fafbfc;
background: var(--color-elevated);
}
.desktop-grid-2 {
+7 -6
View File
@@ -285,16 +285,17 @@
font-weight: 600;
font-size: 0.8125rem;
color: var(--color-text-muted);
background: #f8fafc;
background: var(--color-bg);
border-bottom: 1px solid var(--color-border);
white-space: nowrap;
}
.orders-table td {
padding: 16px;
border-bottom: 1px solid #f1f5f9;
border-bottom: 1px solid var(--color-border);
color: var(--color-text);
vertical-align: middle;
background: var(--color-surface);
}
.orders-table tr:last-child td {
@@ -302,7 +303,7 @@
}
.orders-table tbody tr:hover td {
background: #fafbfc;
background: var(--color-elevated);
}
.orders-table__number {
@@ -362,7 +363,7 @@
}
.orders-table__menu:hover {
background: #f1f5f9;
background: var(--color-elevated);
color: var(--color-text);
}
@@ -548,8 +549,8 @@
margin-inline: 0;
padding: 24px 0 32px;
background: transparent;
height: auto;
overflow: visible;
height: 100%;
overflow: hidden;
}
.order-detail-back {
+2
View File
@@ -225,6 +225,8 @@
max-width: none;
margin-inline: 0;
width: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.order-detail-page__footer {
+6 -65
View File
@@ -1,22 +1,19 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useCartStore } from '@/stores/cart'
import { useNotificationStore } from '@/stores/notification'
import IconSearch from '@/components/icons/IconSearch.vue'
import IconBell from '@/components/icons/IconBell.vue'
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
import AuthThemeToggle from '@/components/ui/AuthThemeToggle.vue'
import { toPersianDigits } from '@/utils/format'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const cartStore = useCartStore()
const notificationStore = useNotificationStore()
const searchQuery = ref('')
const showUserMenu = ref(false)
const unreadLabel = computed(() =>
@@ -35,22 +32,6 @@ const companyLabel = computed(
() => cartStore.companyNamesLabel || cartStore.activeCart?.companyName || '',
)
const searchPlaceholder = computed(() => {
if (route.path.startsWith('/orders')) return 'جستجو در سفارش‌ها...'
if (route.path.startsWith('/companies')) return 'جستجو در محصولات...'
return 'جستجو در سفارش‌ها، شرکت‌ها...'
})
function handleSearch() {
const q = searchQuery.value.trim()
if (!q) return
if (route.path.startsWith('/orders')) {
router.replace({ path: '/orders', query: { ...route.query, q } })
} else if (route.path.includes('/catalogs') && !route.path.includes('/products')) {
router.replace({ query: { ...route.query, search: q } })
}
}
function toggleUserMenu() {
showUserMenu.value = !showUserMenu.value
}
@@ -69,16 +50,7 @@ function logout() {
<template>
<header class="desktop-header">
<div class="desktop-header__search-wrap">
<IconSearch :size="18" class="desktop-header__search-icon" />
<input
v-model="searchQuery"
type="search"
class="desktop-header__search"
:placeholder="searchPlaceholder"
@keydown.enter="handleSearch"
/>
</div>
<div class="desktop-header__spacer" aria-hidden="true" />
<div class="desktop-header__actions">
<span v-if="companyLabel" class="desktop-header__company">{{ companyLabel }}</span>
@@ -150,41 +122,9 @@ function logout() {
}
}
.desktop-header__search-wrap {
.desktop-header__spacer {
flex: 1;
max-width: 420px;
position: relative;
}
.desktop-header__search-icon {
position: absolute;
inset-inline-start: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--color-text-muted);
pointer-events: none;
}
.desktop-header__search {
width: 100%;
height: 42px;
padding-inline: 42px 16px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-summary-bg);
font-family: inherit;
font-size: 0.875rem;
color: var(--color-text);
}
.desktop-header__search:focus {
outline: none;
border-color: var(--color-primary);
background: var(--color-surface);
}
.desktop-header__search::placeholder {
color: var(--color-text-muted);
min-width: 0;
}
.desktop-header__actions {
@@ -192,6 +132,7 @@ function logout() {
align-items: center;
gap: 16px;
flex-shrink: 0;
margin-inline-start: auto;
}
.desktop-header__company {
@@ -262,7 +203,7 @@ function logout() {
height: 32px;
border-radius: 50%;
background: var(--color-primary);
color: #fff;
color: var(--color-surface);
display: flex;
align-items: center;
justify-content: center;
+24 -2
View File
@@ -227,7 +227,8 @@ async function submitChecks() {
<div class="desktop-two-col">
<div class="desktop-two-col__main">
<div class="desktop-table-wrap">
<table class="desktop-table">
<div class="desktop-table-scroll">
<table class="desktop-table check-list-desktop__table">
<thead>
<tr>
<th scope="col">تصویر</th>
@@ -248,7 +249,7 @@ async function submitChecks() {
/>
<span v-else>—</span>
</td>
<td>{{ formatNumber(check.amount) }}</td>
<td class="check-list-desktop__amount">{{ formatNumber(check.amount) }}</td>
<td>{{ formatDate(check.dueDate) }}</td>
<td>چک {{ toPersianDigits(String(index + 1)) }}</td>
<td v-if="isCheckout && orderStore.draftChecks.length > 0">
@@ -260,6 +261,7 @@ async function submitChecks() {
</tbody>
</table>
</div>
</div>
<BaseButton
v-if="isCheckout"
variant="outline"
@@ -314,12 +316,32 @@ async function submitChecks() {
height: 32px;
object-fit: cover;
border-radius: 4px;
display: block;
}
.check-list-desktop__amount {
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.check-list-desktop__table {
min-width: 560px;
}
.check-list-desktop__add {
margin-top: 16px;
}
@media (min-width: 768px) and (max-width: 1099px) {
.check-list-desktop :deep(.desktop-two-col) {
grid-template-columns: 1fr;
}
.check-list-desktop :deep(.desktop-summary-panel) {
position: static;
}
}
.desktop-summary-panel__action {
display: flex;
flex-direction: column;
+134 -62
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useMediaQuery } from '@vueuse/core'
import { useOrderStore } from '@/stores/order'
import BaseButton from '@/components/ui/BaseButton.vue'
import BaseInput from '@/components/ui/BaseInput.vue'
@@ -8,6 +9,7 @@ import CurrencyInput from '@/components/ui/CurrencyInput.vue'
import PersianDateInput from '@/components/ui/PersianDateInput.vue'
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
import {
formatMeterage,
formatCurrency,
@@ -33,11 +35,13 @@ import { isInvoicePriced } from '@/utils/orderInvoice'
const route = useRoute()
const router = useRouter()
const orderStore = useOrderStore()
const isDesktopLayout = useMediaQuery('(min-width: 768px)')
const orderId = computed(() => route.params.id as string)
const order = computed(() => orderStore.getOrderById(orderId.value))
const showTracking = ref(route.query.track === '1' || route.query.receipt === '1')
const showReceiptForm = ref(route.query.receipt === '1')
const showTimeline = ref(true)
const showOrderDetails = ref(route.query.receipt === '1')
const receiptAmount = ref('')
const receiptDate = ref('')
@@ -48,6 +52,19 @@ const receiptImage = ref('')
const fieldErrors = ref<FieldErrors>({})
const formError = ref('')
watch(
isDesktopLayout,
(wide) => {
if (!wide) return
showTracking.value = true
if (route.query.track === '1' || route.query.receipt === '1') return
void router.replace({
query: { ...route.query, track: '1' },
})
},
{ immediate: true },
)
watch(receiptAmount, () => {
fieldErrors.value = clearFieldError(fieldErrors.value, 'receiptAmount')
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
@@ -149,8 +166,12 @@ function openTracking() {
router.replace({ query: { track: '1' } })
}
function openOrderDetails() {
showOrderDetails.value = true
function toggleOrderDetails() {
showOrderDetails.value = !showOrderDetails.value
}
function toggleTimeline() {
showTimeline.value = !showTimeline.value
}
async function handleReceiptImage(event: Event) {
@@ -227,16 +248,14 @@ async function submitReceipt() {
<button type="button" class="order-detail-back" @click="router.push('/orders')">
← بازگشت به سفارش‌ها
</button>
<h1 class="desktop-page__title">
{{ showTracking ? 'پیگیری سفارش' : 'جزئیات سفارش' }}
</h1>
<h1 class="desktop-page__title">پیگیری سفارش</h1>
<p class="desktop-page__subtitle">{{ order.orderNumber }} · {{ order.companyName }}</p>
</header>
<div class="order-detail-page__body">
<p v-if="showTracking" class="order-detail-page__order-id mobile-only">{{ order.orderNumber }}</p>
<div v-if="!showTracking" class="order-info-card">
<div v-if="!showTracking" class="order-info-card mobile-only">
<div class="order-info-card__head">
<span class="order-info-card__label">شماره سفارش</span>
<strong class="order-info-card__number">{{ order.orderNumber }}</strong>
@@ -269,7 +288,7 @@ async function submitReceipt() {
</div>
</div>
<div v-if="nextAction" class="order-next-action">
<div v-if="nextAction && !showTracking" class="order-next-action">
<span class="order-next-action__text">برای ادامه سفارش، این مرحله باقی مانده است.</span>
<BaseButton class="order-next-action__btn" @click="runNextAction">
{{ nextAction.label }}
@@ -278,13 +297,54 @@ async function submitReceipt() {
<div v-if="showTracking" class="order-detail-page__tracking">
<section class="order-detail-page__tracking-main">
<div class="tracking-card">
<h3 class="tracking-card__title">پیگیری وضعیت</h3>
<OrderTrackingTimeline :order="order" />
<div v-if="nextAction" class="order-next-action">
<span class="order-next-action__text">برای ادامه سفارش، این مرحله باقی مانده است.</span>
<BaseButton class="order-next-action__btn" @click="runNextAction">
{{ nextAction.label }}
</BaseButton>
</div>
<template v-if="showOrderDetails">
<div class="tracking-card">
<div class="tracking-card tracking-collapse">
<button
type="button"
class="tracking-collapse__trigger"
:aria-expanded="showTimeline"
@click="toggleTimeline"
>
<span>پیگیری وضعیت</span>
<IconChevronLeft
:size="18"
class="tracking-collapse__chevron"
:class="{ 'tracking-collapse__chevron--open': showTimeline }"
aria-hidden="true"
/>
</button>
<div v-show="showTimeline" class="tracking-collapse__body">
<div class="tracking-collapse__section tracking-collapse__section--flush">
<OrderTrackingTimeline :order="order" />
</div>
</div>
</div>
<div class="tracking-card tracking-collapse">
<button
type="button"
class="tracking-collapse__trigger"
:aria-expanded="showOrderDetails"
@click="toggleOrderDetails"
>
<span>{{ trackingFooterLabel }}</span>
<IconChevronLeft
:size="18"
class="tracking-collapse__chevron"
:class="{ 'tracking-collapse__chevron--open': showOrderDetails }"
aria-hidden="true"
/>
</button>
<div v-show="showOrderDetails" class="tracking-collapse__body">
<div class="tracking-collapse__section">
<h3 class="tracking-card__title">اقلام سفارش</h3>
<div v-for="item in order.items" :key="item.id" class="item-row">
<div>
@@ -304,7 +364,7 @@ async function submitReceipt() {
اعتبار فاکتور: {{ formatDateTime(order.cashInvoiceExpiresAt) }}
</div>
<div class="tracking-card">
<div class="tracking-collapse__section">
<h3 class="tracking-card__title">حساب‌های بانکی</h3>
<div v-for="acc in bankAccounts" :key="acc.id" class="bank-row">
<strong>{{ acc.bankName }}</strong>
@@ -321,7 +381,7 @@ async function submitReceipt() {
{{ order.cashReceipt ? 'ثبت رسید جدید' : 'ثبت رسید پرداخت' }}
</BaseButton>
<div v-if="order.cashReceipt" class="tracking-card">
<div v-if="order.cashReceipt" class="tracking-collapse__section">
<h3 class="tracking-card__title">رسید ثبت‌شده</h3>
<div class="detail-row">
<span>مبلغ</span>
@@ -335,7 +395,7 @@ async function submitReceipt() {
</template>
<template v-if="order.settlementType === 'check' && order.checks">
<div class="tracking-card">
<div class="tracking-collapse__section">
<h3 class="tracking-card__title">رأس چک‌ها</h3>
<div class="rass-display">{{ order.checkRassDays }} روز</div>
<p v-if="order.checkRassDate" class="rass-date">
@@ -343,7 +403,11 @@ async function submitReceipt() {
</p>
</div>
<div v-for="(check, i) in order.checks" :key="check.id" class="tracking-card">
<div
v-for="(check, i) in order.checks"
:key="check.id"
class="tracking-collapse__section"
>
<strong>چک {{ i + 1 }}</strong>
<div class="detail-row">
<span>مبلغ</span>
@@ -356,49 +420,23 @@ async function submitReceipt() {
<img v-if="check.frontImage" :src="check.frontImage" class="check-img" alt="چک" />
</div>
</template>
</template>
</div>
</div>
</section>
<section class="order-detail-page__tracking-aside">
<div class="tracking-card tracking-card--hero">
<OrderStatusHero v-if="statusHero" :config="statusHero" />
<OrderTrackingSummary :order="order" />
<div class="order-detail-page__tracking-cta desktop-only">
<BaseButton
v-if="!showOrderDetails"
block
@click="openOrderDetails"
>
{{ trackingFooterLabel }}
</BaseButton>
<BaseButton
v-else
block
variant="outline"
@click="showOrderDetails = false"
>
بستن جزئیات
</BaseButton>
</div>
</div>
</section>
</div>
</div>
<div v-if="showTracking && !showOrderDetails" class="order-detail-page__footer mobile-only">
<BaseButton block @click="openOrderDetails">{{ trackingFooterLabel }}</BaseButton>
</div>
<div v-if="!showTracking" class="order-detail-page__footer mobile-only">
<BaseButton block @click="openTracking">مشاهده جزئیات و پیگیری</BaseButton>
</div>
<div v-if="!showTracking" class="order-detail-page__desktop-actions desktop-only">
<BaseButton class="desktop-btn-compact" @click="openTracking">
مشاهده جزئیات و پیگیری
</BaseButton>
</div>
<div v-if="showReceiptForm" class="dialog-overlay" @click.self="showReceiptForm = false">
<div class="dialog card">
<h3>ثبت رسید پرداخت</h3>
@@ -473,8 +511,6 @@ async function submitReceipt() {
max-width: none;
margin-inline: 0;
width: 100%;
height: auto;
overflow: visible;
}
}
@@ -521,7 +557,7 @@ async function submitReceipt() {
min-height: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding-bottom: 16px;
padding-bottom: 28px;
width: 100%;
}
@@ -714,19 +750,62 @@ async function submitReceipt() {
font-weight: 700;
}
.order-detail-page__tracking-cta {
padding-top: 4px;
.tracking-collapse {
padding: 0;
overflow: hidden;
}
.order-detail-page__desktop-actions {
display: none;
.tracking-collapse__trigger {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border: none;
background: transparent;
color: var(--color-text);
font-family: inherit;
font-size: 0.9375rem;
font-weight: 700;
text-align: start;
cursor: pointer;
transition: background 0.15s;
}
.tracking-collapse__trigger:hover {
background: var(--color-bg);
}
.tracking-collapse__chevron {
flex-shrink: 0;
color: var(--color-text-muted);
transform: rotate(-90deg);
transition: transform 0.2s ease;
}
.tracking-collapse__chevron--open {
transform: rotate(90deg);
color: var(--color-primary);
}
.tracking-collapse__body {
display: flex;
flex-direction: column;
gap: 12px;
padding: 0 16px 16px;
border-top: 1px solid var(--color-border);
}
.tracking-collapse__section {
padding-top: 12px;
}
.tracking-collapse__section--flush {
padding-top: 8px;
}
@media (min-width: 768px) {
.order-detail-page__body {
overflow: visible;
}
.order-detail-page__tracking {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(260px, 0.9fr);
@@ -741,12 +820,6 @@ async function submitReceipt() {
top: 16px;
}
.order-detail-page__desktop-actions {
display: flex;
justify-content: flex-start;
margin-top: 16px;
}
.order-detail-page__footer {
display: none;
}
@@ -789,7 +862,6 @@ async function submitReceipt() {
.item-row--summary {
margin-top: 4px;
padding-top: 12px;
border-top: 1px solid var(--color-border);
border-bottom: none;
}
@@ -176,7 +176,7 @@ const emit = defineEmits<{
aria-label="صفحه قبل"
@click="emit('setPage', currentPage - 1)"
>
<IconChevronLeft :size="16" class="orders-pagination__arrow-icon--next" />
<IconChevronLeft :size="16" class="orders-pagination__arrow-icon--prev" />
</button>
<template v-for="(page, index) in visiblePages" :key="`${page}-${index}`">
<span v-if="page === 'ellipsis'" class="orders-pagination__ellipsis">…</span>
@@ -197,7 +197,7 @@ const emit = defineEmits<{
aria-label="صفحه بعد"
@click="emit('setPage', currentPage + 1)"
>
<IconChevronLeft :size="16" class="orders-pagination__arrow-icon--prev" />
<IconChevronLeft :size="16" class="orders-pagination__arrow-icon--next" />
</button>
</div>
</div>
@@ -94,7 +94,7 @@ const emit = defineEmits<{
aria-label="صفحه قبل"
@click="emit('set-page', currentPage - 1)"
>
<IconChevronLeft :size="16" class="orders-mobile-pagination__arrow--next" />
<IconChevronLeft :size="16" class="orders-mobile-pagination__arrow--prev" />
</button>
<template v-for="(page, index) in visiblePages" :key="`${page}-${index}`">
<span v-if="page === 'ellipsis'" class="orders-mobile-pagination__ellipsis">…</span>
@@ -115,7 +115,7 @@ const emit = defineEmits<{
aria-label="صفحه بعد"
@click="emit('set-page', currentPage + 1)"
>
<IconChevronLeft :size="16" class="orders-mobile-pagination__arrow--prev" />
<IconChevronLeft :size="16" class="orders-mobile-pagination__arrow--next" />
</button>
</div>
</div>
@@ -307,6 +307,10 @@ const emit = defineEmits<{
transform: scaleX(-1);
}
.orders-mobile-pagination__arrow--next {
transform: none;
}
.orders-mobile-pagination__page--active {
background: var(--color-primary);
border-color: var(--color-primary);