Fix order tracking desktop layout and gate settlement until invoice pricing.
Show sidebar labels on tablet, improve theme-safe UI, and keep Jalali dates on tracking. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -544,44 +544,19 @@
|
||||
|
||||
/* Order detail desktop */
|
||||
.order-detail-page {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
padding: 24px var(--page-padding-desktop) 32px;
|
||||
max-width: none;
|
||||
margin-inline: 0;
|
||||
padding: 24px 0 32px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 340px;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking > .status-hero {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking > .order-detail-back {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking > .tracking-card:first-of-type {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking > .tracking-summary {
|
||||
grid-column: 2;
|
||||
grid-row: 2 / span 2;
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
align-self: start;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.order-detail-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
@@ -222,39 +222,15 @@
|
||||
|
||||
/* Order detail & tracking */
|
||||
.order-detail-page__body {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
max-width: none;
|
||||
margin-inline: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 340px);
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking > .status-hero {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking > .tracking-card {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking > .tracking-summary {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.order-detail-page__footer {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
max-width: none;
|
||||
margin-inline: 0;
|
||||
width: 100%;
|
||||
padding-inline: var(--page-padding);
|
||||
}
|
||||
|
||||
/* Checkout forms */
|
||||
|
||||
@@ -5,7 +5,7 @@ defineProps<{
|
||||
|
||||
const steps = [
|
||||
{ num: 1, label: 'سبد خرید' },
|
||||
{ num: 2, label: 'ثبت سفارش' },
|
||||
{ num: 2, label: 'پیشسفارش' },
|
||||
{ num: 3, label: 'صدور فاکتور' },
|
||||
{ num: 4, label: 'تسویه' },
|
||||
]
|
||||
|
||||
@@ -7,6 +7,7 @@ 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()
|
||||
@@ -82,6 +83,8 @@ function logout() {
|
||||
<div class="desktop-header__actions">
|
||||
<span v-if="companyLabel" class="desktop-header__company">{{ companyLabel }}</span>
|
||||
|
||||
<AuthThemeToggle variant="inline" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="desktop-header__icon-btn"
|
||||
@@ -168,7 +171,7 @@ function logout() {
|
||||
padding-inline: 42px 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: #f9fafb;
|
||||
background: var(--color-summary-bg);
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text);
|
||||
@@ -312,7 +315,7 @@ function logout() {
|
||||
}
|
||||
|
||||
.desktop-header__menu button:hover {
|
||||
background: #f8fafc;
|
||||
background: var(--color-elevated);
|
||||
}
|
||||
|
||||
.desktop-header__menu-logout {
|
||||
|
||||
@@ -75,12 +75,12 @@ function navigate(item: (typeof DESKTOP_NAV_ITEMS)[number]) {
|
||||
.desktop-sidebar {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
width: var(--sidebar-width-compact);
|
||||
width: var(--sidebar-width);
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
background: var(--color-surface);
|
||||
border-inline-start: 1px solid var(--color-border);
|
||||
padding: 16px 8px;
|
||||
padding: 20px 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -92,7 +92,6 @@ function navigate(item: (typeof DESKTOP_NAV_ITEMS)[number]) {
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.desktop-sidebar {
|
||||
width: var(--sidebar-width);
|
||||
padding: 24px 16px;
|
||||
}
|
||||
}
|
||||
@@ -100,16 +99,15 @@ function navigate(item: (typeof DESKTOP_NAV_ITEMS)[number]) {
|
||||
.desktop-sidebar__brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
padding: 0 4px 20px;
|
||||
padding: 0 8px 20px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.desktop-sidebar__brand {
|
||||
justify-content: flex-start;
|
||||
padding: 0 12px 24px;
|
||||
}
|
||||
}
|
||||
@@ -119,7 +117,7 @@ function navigate(item: (typeof DESKTOP_NAV_ITEMS)[number]) {
|
||||
height: 36px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
color: var(--color-surface);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -129,18 +127,12 @@ function navigate(item: (typeof DESKTOP_NAV_ITEMS)[number]) {
|
||||
}
|
||||
|
||||
.desktop-sidebar__title {
|
||||
display: none;
|
||||
display: block;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.desktop-sidebar__title {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-sidebar__nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -151,10 +143,10 @@ function navigate(item: (typeof DESKTOP_NAV_ITEMS)[number]) {
|
||||
.desktop-sidebar__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
padding: 11px 12px;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
background: none;
|
||||
@@ -169,16 +161,20 @@ function navigate(item: (typeof DESKTOP_NAV_ITEMS)[number]) {
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.desktop-sidebar__item {
|
||||
justify-content: flex-start;
|
||||
padding: 11px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-sidebar__item:hover {
|
||||
background: #f8fafc;
|
||||
background: var(--color-elevated);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.desktop-sidebar__item:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.desktop-sidebar__item.active {
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
@@ -200,57 +196,41 @@ function navigate(item: (typeof DESKTOP_NAV_ITEMS)[number]) {
|
||||
}
|
||||
|
||||
.desktop-sidebar__label {
|
||||
display: none;
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.desktop-sidebar__label {
|
||||
display: block;
|
||||
}
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.desktop-sidebar__badge {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
inset-inline-end: 6px;
|
||||
margin-inline-start: auto;
|
||||
background: var(--color-danger);
|
||||
color: #fff;
|
||||
font-size: 0.625rem;
|
||||
color: var(--color-surface);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.desktop-sidebar__badge {
|
||||
position: static;
|
||||
margin-inline-start: auto;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.desktop-sidebar__support {
|
||||
display: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
padding: 16px;
|
||||
padding: 14px;
|
||||
background: var(--color-primary-soft);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.desktop-sidebar__support {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +239,7 @@ function navigate(item: (typeof DESKTOP_NAV_ITEMS)[number]) {
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
color: var(--color-surface);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -283,7 +263,8 @@ function navigate(item: (typeof DESKTOP_NAV_ITEMS)[number]) {
|
||||
margin-top: 4px;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: #fff;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-primary);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -45,7 +45,7 @@ defineProps<{
|
||||
|
||||
.status-hero--success .status-hero__icon {
|
||||
background: var(--color-success);
|
||||
color: #fff;
|
||||
color: var(--color-text-on-primary, #fff);
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { computed } from 'vue'
|
||||
import type { Order, OrderStatus } from '@/types'
|
||||
import { ORDER_TRACKING_STEPS } from '@/utils/constants'
|
||||
import { getStepTimestamp, getTrackingStepIndex } from '@/utils/orderTracking'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{
|
||||
order: Order
|
||||
@@ -16,24 +17,9 @@ function stepState(index: number): 'done' | 'current' | 'pending' {
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
function formatStepTime(date: string | null) {
|
||||
if (!date) return null
|
||||
const d = new Date(date)
|
||||
const datePart = new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).format(d)
|
||||
const timePart = new Intl.DateTimeFormat('fa-IR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}).format(d)
|
||||
return `${datePart} - ${timePart}`
|
||||
}
|
||||
|
||||
function timestampFor(step: OrderStatus) {
|
||||
return formatStepTime(getStepTimestamp(props.order, step))
|
||||
const raw = getStepTimestamp(props.order, step)
|
||||
return raw ? formatDateTime(raw) : null
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -46,13 +32,16 @@ function timestampFor(step: OrderStatus) {
|
||||
:class="`tracking-timeline__item--${stepState(index)}`"
|
||||
>
|
||||
<div class="tracking-timeline__marker-wrap">
|
||||
<div class="tracking-timeline__line" />
|
||||
<div class="tracking-timeline__line" aria-hidden="true" />
|
||||
<div class="tracking-timeline__marker">
|
||||
<span v-if="stepState(index) === 'done'" class="tracking-timeline__check">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tracking-timeline__content">
|
||||
<div
|
||||
class="tracking-timeline__content"
|
||||
:class="{ 'tracking-timeline__content--current': stepState(index) === 'current' }"
|
||||
>
|
||||
<strong class="tracking-timeline__label">{{ step.label }}</strong>
|
||||
<span v-if="timestampFor(step.status)" class="tracking-timeline__time">
|
||||
{{ timestampFor(step.status) }}
|
||||
@@ -66,36 +55,34 @@ function timestampFor(step: OrderStatus) {
|
||||
.tracking-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.tracking-timeline__item {
|
||||
display: grid;
|
||||
grid-template-columns: 28px 1fr;
|
||||
gap: 12px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.tracking-timeline__item--current {
|
||||
margin: 4px 0;
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
background: var(--color-primary-soft);
|
||||
border: 1px solid var(--color-border);
|
||||
grid-template-columns: 28px minmax(0, 1fr);
|
||||
column-gap: 12px;
|
||||
align-items: start;
|
||||
/* Same vertical rhythm for every step so the connector stays continuous */
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.tracking-timeline__marker-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-self: stretch;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.tracking-timeline__line {
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
bottom: -10px;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 2px;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-border);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.tracking-timeline__item:last-child .tracking-timeline__line {
|
||||
@@ -106,16 +93,22 @@ function timestampFor(step: OrderStatus) {
|
||||
background: var(--color-success);
|
||||
}
|
||||
|
||||
.tracking-timeline__item--current .tracking-timeline__line {
|
||||
background: color-mix(in srgb, var(--color-primary) 35%, var(--color-border));
|
||||
}
|
||||
|
||||
.tracking-timeline__marker {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-top: 2px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-border);
|
||||
border: 2px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -127,7 +120,7 @@ function timestampFor(step: OrderStatus) {
|
||||
.tracking-timeline__item--current .tracking-timeline__marker {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 4px var(--color-primary-soft);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 28%, transparent);
|
||||
}
|
||||
|
||||
.tracking-timeline__item--pending .tracking-timeline__marker {
|
||||
@@ -136,7 +129,7 @@ function timestampFor(step: OrderStatus) {
|
||||
}
|
||||
|
||||
.tracking-timeline__check {
|
||||
color: #fff;
|
||||
color: var(--color-surface);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
@@ -144,7 +137,14 @@ function timestampFor(step: OrderStatus) {
|
||||
|
||||
.tracking-timeline__content {
|
||||
min-width: 0;
|
||||
padding-top: 1px;
|
||||
padding: 2px 0 14px;
|
||||
}
|
||||
|
||||
.tracking-timeline__content--current {
|
||||
padding: 8px 12px 10px;
|
||||
border-radius: 12px;
|
||||
background: var(--color-primary-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--color-primary) 28%, var(--color-border));
|
||||
}
|
||||
|
||||
.tracking-timeline__label {
|
||||
|
||||
@@ -4,6 +4,14 @@ import { storeToRefs } from 'pinia'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
/** floating = auth pages; inline = header / toolbar */
|
||||
variant?: 'floating' | 'inline'
|
||||
}>(),
|
||||
{ variant: 'floating' },
|
||||
)
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
const { isDark } = storeToRefs(themeStore)
|
||||
|
||||
@@ -19,31 +27,24 @@ const themeLabel = computed(() =>
|
||||
<button
|
||||
type="button"
|
||||
class="auth-theme-toggle"
|
||||
:class="variant === 'inline' ? 'auth-theme-toggle--inline' : 'auth-theme-toggle--floating'"
|
||||
:aria-label="themeLabel"
|
||||
:title="themeLabel"
|
||||
@click="themeStore.toggleLightDark()"
|
||||
>
|
||||
<BasicIcon :icon="themeIcon" :size="22" :stroke-width="1.5" />
|
||||
<BasicIcon :icon="themeIcon" :size="variant === 'inline' ? 20 : 22" :stroke-width="1.5" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.auth-theme-toggle {
|
||||
position: fixed;
|
||||
top: calc(12px + env(safe-area-inset-top, 0px));
|
||||
inset-inline-end: 16px;
|
||||
z-index: 100;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
box-shadow: var(--shadow-sm);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
@@ -51,6 +52,26 @@ const themeLabel = computed(() =>
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.auth-theme-toggle--floating {
|
||||
position: fixed;
|
||||
top: calc(12px + env(safe-area-inset-top, 0px));
|
||||
inset-inline-end: 16px;
|
||||
z-index: 100;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.auth-theme-toggle--inline {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-muted);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.auth-theme-toggle:hover {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-soft);
|
||||
|
||||
@@ -65,7 +65,7 @@ defineProps<{
|
||||
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
color: var(--color-surface);
|
||||
}
|
||||
|
||||
.btn-primary:not(:disabled):hover {
|
||||
@@ -85,7 +85,7 @@ defineProps<{
|
||||
|
||||
.btn-danger {
|
||||
background: var(--color-danger);
|
||||
color: #fff;
|
||||
color: var(--color-surface);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
|
||||
@@ -15,8 +15,8 @@ defineProps<{
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
background: var(--color-danger-light);
|
||||
border: 1px solid color-mix(in srgb, var(--color-danger) 40%, transparent);
|
||||
color: var(--color-danger);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import { isInvoicePriced } from '@/utils/orderInvoice'
|
||||
import type { Order } from '@/types'
|
||||
|
||||
/**
|
||||
* مسیرهای تسویه/چک فقط بعد از قیمتگذاری ادمین مجازند.
|
||||
* در غیر این صورت به جزئیات سفارش برمیگردیم.
|
||||
*/
|
||||
export function useRequirePricedOrder() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const orderId = computed(() => route.params.id as string)
|
||||
const order = computed(() => orderStore.getOrderById(orderId.value))
|
||||
const invoiceReady = computed(() => isInvoicePriced(order.value))
|
||||
|
||||
function redirectIfNeeded(current: Order | null | undefined) {
|
||||
if (!current) return
|
||||
if (!isInvoicePriced(current)) {
|
||||
router.replace(`/orders/${current.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
orderId,
|
||||
async (id) => {
|
||||
if (!id) return
|
||||
const current = orderStore.getOrderById(id) ?? (await orderStore.fetchOrder(id))
|
||||
redirectIfNeeded(current)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(order, (current) => {
|
||||
redirectIfNeeded(current)
|
||||
})
|
||||
|
||||
return { orderId, order, invoiceReady }
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useRoute } from 'vue-router'
|
||||
import BottomNav from '@/components/layout/BottomNav.vue'
|
||||
import DesktopSidebar from '@/components/layout/DesktopSidebar.vue'
|
||||
import DesktopHeader from '@/components/layout/DesktopHeader.vue'
|
||||
import AuthThemeToggle from '@/components/ui/AuthThemeToggle.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -57,5 +58,6 @@ const isWideGuest = computed(() => {
|
||||
<BottomNav v-if="showNav" />
|
||||
</div>
|
||||
</div>
|
||||
<AuthThemeToggle v-if="showNav" class="mobile-only" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -43,6 +43,14 @@ const persianDateTimeFormatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
|
||||
minute: '2-digit',
|
||||
})
|
||||
|
||||
function isJalaliDateString(value: string): boolean {
|
||||
const normalized = normalizeDigits(value.trim()).replace(/-/g, '/')
|
||||
const match = normalized.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})/)
|
||||
if (!match) return false
|
||||
const year = Number(match[1])
|
||||
return year >= 1300 && year <= 1500
|
||||
}
|
||||
|
||||
function toDateValue(date: string | Date): Date {
|
||||
if (typeof date !== 'string') return date
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(date)) return parseISODate(date)
|
||||
@@ -51,6 +59,13 @@ function toDateValue(date: string | Date): Date {
|
||||
|
||||
export function formatDate(date: string | Date | null | undefined): string {
|
||||
if (!date) return '—'
|
||||
if (typeof date === 'string' && isJalaliDateString(date)) {
|
||||
const normalized = normalizeDigits(date.trim()).replace(/-/g, '/')
|
||||
const match = normalized.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})/)
|
||||
if (!match) return toPersianDigits(normalized)
|
||||
const [, y, m, d] = match
|
||||
return toPersianDigits(`${y}/${m!.padStart(2, '0')}/${d!.padStart(2, '0')}`)
|
||||
}
|
||||
const value = toDateValue(date)
|
||||
if (Number.isNaN(value.getTime())) return '—'
|
||||
return persianDateFormatter.format(value)
|
||||
@@ -58,6 +73,9 @@ export function formatDate(date: string | Date | null | undefined): string {
|
||||
|
||||
export function formatDateTime(date: string | Date | null | undefined): string {
|
||||
if (!date) return '—'
|
||||
if (typeof date === 'string' && isJalaliDateString(date) && !/[T\s]\d/.test(date)) {
|
||||
return formatDate(date)
|
||||
}
|
||||
const value = toDateValue(date)
|
||||
if (Number.isNaN(value.getTime())) return '—'
|
||||
return persianDateTimeFormatter.format(value)
|
||||
@@ -65,6 +83,7 @@ export function formatDateTime(date: string | Date | null | undefined): string {
|
||||
|
||||
export function formatTime(date: string | Date): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date
|
||||
if (Number.isNaN(d.getTime())) return '—'
|
||||
return new Intl.DateTimeFormat('fa-IR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Order, OrderStatus } from '@/types'
|
||||
|
||||
/** سفارش وقتی قابل تسویه/چک است که مبلغ فاکتور توسط ادمین تعیین شده باشد. */
|
||||
export function isInvoicePriced(order: Order | null | undefined): boolean {
|
||||
return !!order && order.finalAmount != null && Number(order.finalAmount) > 0
|
||||
}
|
||||
|
||||
const SETTLEMENT_STATUSES: OrderStatus[] = [
|
||||
'awaiting_settlement',
|
||||
'awaiting_settlement_fix',
|
||||
'cash_invoice_issued',
|
||||
]
|
||||
|
||||
const CHECK_ENTRY_STATUSES: OrderStatus[] = [
|
||||
'check_in_progress',
|
||||
'check_needs_fix',
|
||||
'check_rejected',
|
||||
'awaiting_settlement',
|
||||
'awaiting_settlement_fix',
|
||||
]
|
||||
|
||||
const PRE_PRICE_STATUSES: OrderStatus[] = [
|
||||
'submitted',
|
||||
'awaiting_review',
|
||||
'pricing',
|
||||
'needs_correction',
|
||||
]
|
||||
|
||||
export function canChooseSettlement(order: Order | null | undefined): boolean {
|
||||
return isInvoicePriced(order) && !!order && SETTLEMENT_STATUSES.includes(order.status)
|
||||
}
|
||||
|
||||
export function canEnterChecks(order: Order | null | undefined): boolean {
|
||||
if (!isInvoicePriced(order) || !order) return false
|
||||
if (CHECK_ENTRY_STATUSES.includes(order.status)) return true
|
||||
return order.settlementType === 'check' && order.status === 'check_in_progress'
|
||||
}
|
||||
|
||||
export function isAwaitingAdminPricing(order: Order | null | undefined): boolean {
|
||||
if (!order) return false
|
||||
if (isInvoicePriced(order)) return false
|
||||
return PRE_PRICE_STATUSES.includes(order.status) || !order.finalAmount
|
||||
}
|
||||
@@ -27,15 +27,33 @@ const STATUS_STEP_INDEX: Partial<Record<OrderStatus, number>> = {
|
||||
}
|
||||
|
||||
const STATUS_HERO: Partial<Record<OrderStatus, StatusHeroConfig>> = {
|
||||
submitted: {
|
||||
variant: 'waiting',
|
||||
title: 'پیشسفارش ثبت شد',
|
||||
description:
|
||||
'سفارش شما برای بررسی و قیمتگذاری به مدیریت ارسال شد. پس از صدور فاکتور میتوانید روش تسویه (نقد یا چک) را انتخاب کنید.',
|
||||
},
|
||||
awaiting_review: {
|
||||
variant: 'waiting',
|
||||
title: 'در انتظار بررسی مدیریت',
|
||||
description: 'سفارش شما ثبت شده و در صف بررسی مدیریت قرار دارد. پس از بررسی، نتیجه از طریق همین صفحه اطلاعرسانی میشود.',
|
||||
description:
|
||||
'پیشسفارش شما در صف بررسی است. پس از قیمتگذاری و صدور فاکتور، از طریق همین صفحه برای انتخاب تسویه مطلع میشوید.',
|
||||
},
|
||||
pricing: {
|
||||
variant: 'waiting',
|
||||
title: 'در حال قیمتگذاری',
|
||||
description: 'مدیریت در حال تعیین مبلغ نهایی فاکتور است. لطفاً کمی صبر کنید.',
|
||||
description:
|
||||
'مدیریت در حال تعیین مبلغ نهایی فاکتور است. تا قبل از صدور فاکتور امکان ثبت چک یا پرداخت وجود ندارد.',
|
||||
},
|
||||
awaiting_settlement: {
|
||||
variant: 'info',
|
||||
title: 'فاکتور آماده است',
|
||||
description: 'مبلغ فاکتور مشخص شده. لطفاً روش تسویه را انتخاب کنید و مسیر پرداخت را ادامه دهید.',
|
||||
},
|
||||
awaiting_settlement_fix: {
|
||||
variant: 'info',
|
||||
title: 'نیاز به اصلاح تسویه',
|
||||
description: 'لطفاً روش تسویه یا اطلاعات پرداخت را اصلاح کنید.',
|
||||
},
|
||||
awaiting_check_review: {
|
||||
variant: 'waiting',
|
||||
|
||||
@@ -173,7 +173,7 @@ const quickLinks = [
|
||||
@click="emit('checkout')"
|
||||
>
|
||||
<IconChevronLeft :size="16" />
|
||||
{{ submitting ? 'در حال ثبت…' : 'ثبت سفارش' }}
|
||||
{{ submitting ? 'در حال ثبت…' : 'ثبت پیشسفارش' }}
|
||||
</button>
|
||||
<p class="cart-summary-card__note">
|
||||
قیمتگذاری توسط مدیریت انجام میشود؛ پس از صدور فاکتور روش تسویه را انتخاب میکنید.
|
||||
@@ -366,7 +366,7 @@ const quickLinks = [
|
||||
.cart-btn--primary {
|
||||
border: none;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
color: var(--color-surface);
|
||||
}
|
||||
|
||||
.cart-btn--primary:hover {
|
||||
@@ -375,7 +375,7 @@ const quickLinks = [
|
||||
|
||||
.cart-btn--outline {
|
||||
border: 1px solid var(--color-border);
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
@@ -512,15 +512,15 @@ const quickLinks = [
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
border: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
color: var(--color-surface);
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.cart-summary-card__cta:hover:not(:disabled) {
|
||||
@@ -528,8 +528,9 @@ const quickLinks = [
|
||||
}
|
||||
|
||||
.cart-summary-card__cta:disabled {
|
||||
background: #e8ecf3;
|
||||
color: #94a3b8;
|
||||
background: var(--color-elevated);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text-muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ const emit = defineEmits<{
|
||||
پس از ثبت، مدیریت سفارش را قیمتگذاری میکند و فاکتور برای انتخاب روش تسویه به شما اعلام
|
||||
میشود.
|
||||
</p>
|
||||
<BaseButton block :loading="submitting" @click="emit('checkout')">ثبت سفارش</BaseButton>
|
||||
<BaseButton block :loading="submitting" @click="emit('checkout')">ثبت پیشسفارش</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import { useCatalogStore } from '@/stores/catalog'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import IconSuccessCircle from '@/components/icons/IconSuccessCircle.vue'
|
||||
import { formatCurrency, formatDateTime } from '@/utils/format'
|
||||
import { useRequirePricedOrder } from '@/composables/useRequirePricedOrder'
|
||||
import type { BankAccount } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const orderStore = useOrderStore()
|
||||
const catalogStore = useCatalogStore()
|
||||
const bankAccounts = ref<BankAccount[]>([])
|
||||
|
||||
const orderId = computed(() => route.params.id as string)
|
||||
const order = computed(() => orderStore.getOrderById(orderId.value))
|
||||
const { orderId, order } = useRequirePricedOrder()
|
||||
const companyId = computed(() => order.value?.companyId ?? '')
|
||||
const companyName = computed(() => order.value?.companyName ?? '')
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import BaseInput from '@/components/ui/BaseInput.vue'
|
||||
@@ -21,12 +21,12 @@ import {
|
||||
validateImageFile,
|
||||
fileToBase64,
|
||||
} from '@/utils/validation'
|
||||
import { useRequirePricedOrder } from '@/composables/useRequirePricedOrder'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const orderId = computed(() => route.params.id as string)
|
||||
const { orderId } = useRequirePricedOrder()
|
||||
const isFull = computed(() => orderStore.draftCheckMode === 'full')
|
||||
|
||||
const amount = ref('')
|
||||
|
||||
@@ -10,6 +10,7 @@ import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import { formatCurrency, formatDate, formatNumber, toPersianDigits } from '@/utils/format'
|
||||
import { calculateCheckRass } from '@/utils/checkRass'
|
||||
import { confirm } from '@/composables/useConfirm'
|
||||
import { useRequirePricedOrder } from '@/composables/useRequirePricedOrder'
|
||||
import type { CheckData } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -20,7 +21,7 @@ const formError = ref('')
|
||||
const submitting = ref(false)
|
||||
|
||||
const isCheckout = computed(() => route.name === 'check-list')
|
||||
const orderId = computed(() => (route.params.id as string | undefined) ?? '')
|
||||
const { orderId } = useRequirePricedOrder()
|
||||
const order = computed(() =>
|
||||
orderId.value ? orderStore.getOrderById(orderId.value) : undefined,
|
||||
)
|
||||
@@ -201,6 +202,9 @@ async function submitChecks() {
|
||||
</div>
|
||||
|
||||
<div v-if="isCheckout && orderStore.draftChecks.length > 0" class="check-list-page__footer">
|
||||
<BaseButton block variant="outline" :disabled="submitting" @click="addAnother">
|
||||
افزودن چک جدید
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
block
|
||||
:disabled="checks.length === 0"
|
||||
@@ -281,6 +285,9 @@ async function submitChecks() {
|
||||
<strong>{{ toPersianDigits(String(rass.rassDays)) }} روز</strong>
|
||||
</div>
|
||||
<div v-if="isCheckout && orderStore.draftChecks.length > 0" class="desktop-summary-panel__action">
|
||||
<BaseButton block variant="outline" :disabled="submitting" @click="addAnother">
|
||||
افزودن چک جدید
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
block
|
||||
:disabled="checks.length === 0"
|
||||
@@ -313,6 +320,12 @@ async function submitChecks() {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.desktop-summary-panel__action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.check-list-page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -577,6 +590,9 @@ async function submitChecks() {
|
||||
|
||||
.check-list-page__footer {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px 0 calc(12px + env(safe-area-inset-bottom, 0));
|
||||
background: var(--color-bg);
|
||||
border-top: 1px solid var(--color-border);
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import IconCheckBook from '@/components/icons/IconCheckBook.vue'
|
||||
import IconEditPen from '@/components/icons/IconEditPen.vue'
|
||||
import { useRequirePricedOrder } from '@/composables/useRequirePricedOrder'
|
||||
import { formatCurrency } from '@/utils/format'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const orderId = computed(() => route.params.id as string)
|
||||
const { orderId, order } = useRequirePricedOrder()
|
||||
|
||||
const invoiceLabel = computed(() =>
|
||||
order.value?.finalAmount ? formatCurrency(order.value.finalAmount) : '—',
|
||||
)
|
||||
|
||||
watch(
|
||||
orderId,
|
||||
@@ -32,13 +37,18 @@ function selectMode(mode: 'full' | 'brief') {
|
||||
<div class="page check-mode-page">
|
||||
<PageHeader
|
||||
title="تسویه چکی"
|
||||
subtitle="نحوه ثبت اطلاعات چکها را انتخاب کنید"
|
||||
subtitle="بر اساس مبلغ فاکتور، نحوه ثبت چک را انتخاب کنید"
|
||||
show-back
|
||||
centered
|
||||
@back="router.push(`/orders/${orderId}`)"
|
||||
/>
|
||||
|
||||
<div class="page__scroll check-mode-page__scroll">
|
||||
<div class="check-mode-invoice">
|
||||
<span class="check-mode-invoice__label">مبلغ فاکتور</span>
|
||||
<strong class="check-mode-invoice__value">{{ invoiceLabel }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="mode-options">
|
||||
<button type="button" class="mode-card" @click="selectMode('full')">
|
||||
<span class="mode-card__icon">
|
||||
@@ -69,6 +79,29 @@ function selectMode(mode: 'full' | 'brief') {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.check-mode-invoice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 16px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 16px;
|
||||
background: var(--color-primary-light);
|
||||
border: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.check-mode-invoice__label {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.check-mode-invoice__value {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.mode-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -95,7 +128,7 @@ function selectMode(mode: 'full' | 'brief') {
|
||||
}
|
||||
|
||||
.mode-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.1);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.mode-card:active {
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import CheckoutStepper from '@/components/layout/CheckoutStepper.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import IconCashSettlement from '@/components/icons/IconCashSettlement.vue'
|
||||
import IconCheckBook from '@/components/icons/IconCheckBook.vue'
|
||||
import { formatCurrency } from '@/utils/format'
|
||||
import { isInvoicePriced } from '@/utils/orderInvoice'
|
||||
import { useRequirePricedOrder } from '@/composables/useRequirePricedOrder'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const orderId = computed(() => route.params.id as string)
|
||||
const order = computed(() => orderStore.getOrderById(orderId.value))
|
||||
const { orderId, order, invoiceReady } = useRequirePricedOrder()
|
||||
const submitting = ref(false)
|
||||
const formError = ref('')
|
||||
|
||||
@@ -31,7 +32,10 @@ const invoiceLabel = computed(() =>
|
||||
)
|
||||
|
||||
async function selectType(type: 'cash' | 'check') {
|
||||
if (submitting.value) return
|
||||
if (submitting.value || !isInvoicePriced(order.value)) {
|
||||
formError.value = 'تا قبل از قیمتگذاری مدیریت، امکان انتخاب روش تسویه وجود ندارد.'
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
formError.value = ''
|
||||
try {
|
||||
@@ -55,7 +59,7 @@ async function selectType(type: 'cash' | 'check') {
|
||||
<div class="page settlement-mobile-page mobile-only">
|
||||
<PageHeader
|
||||
title="انتخاب نوع تسویه"
|
||||
subtitle="نوع تسویه را انتخاب کنید"
|
||||
subtitle="پس از صدور فاکتور، نوع تسویه را انتخاب کنید"
|
||||
show-back
|
||||
centered
|
||||
@back="router.push(`/orders/${orderId}`)"
|
||||
@@ -63,38 +67,51 @@ async function selectType(type: 'cash' | 'check') {
|
||||
<div class="page__scroll">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<div class="settlement-invoice">
|
||||
<span class="settlement-invoice__label">مبلغ فاکتور</span>
|
||||
<strong class="settlement-invoice__value">{{ invoiceLabel }}</strong>
|
||||
<span v-if="order" class="settlement-invoice__meta">سفارش {{ order.orderNumber }}</span>
|
||||
<div v-if="!invoiceReady" class="settlement-waiting">
|
||||
<h3 class="settlement-waiting__title">در انتظار قیمتگذاری</h3>
|
||||
<p class="settlement-waiting__text">
|
||||
پیشسفارش ثبت شده است. پس از تعیین قیمت توسط مدیریت و صدور فاکتور، از این صفحه روش
|
||||
تسویه (نقد یا چک) را انتخاب کنید.
|
||||
</p>
|
||||
<BaseButton block variant="outline" @click="router.push(`/orders/${orderId}`)">
|
||||
بازگشت به جزئیات سفارش
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="settlement-mobile-options">
|
||||
<button
|
||||
type="button"
|
||||
class="settlement-card"
|
||||
:disabled="submitting"
|
||||
@click="selectType('cash')"
|
||||
>
|
||||
<span class="settlement-card__icon settlement-card__icon--cash">
|
||||
<IconCashSettlement :size="36" />
|
||||
</span>
|
||||
<h3>تسویه نقدی</h3>
|
||||
<p>پرداخت از طریق واریز به حساب شرکت</p>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="settlement-card"
|
||||
:disabled="submitting"
|
||||
@click="selectType('check')"
|
||||
>
|
||||
<span class="settlement-card__icon settlement-card__icon--check">
|
||||
<IconCheckBook :size="36" />
|
||||
</span>
|
||||
<h3>تسویه چکی</h3>
|
||||
<p>پرداخت از طریق ثبت چک</p>
|
||||
</button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="settlement-invoice">
|
||||
<span class="settlement-invoice__label">مبلغ فاکتور</span>
|
||||
<strong class="settlement-invoice__value">{{ invoiceLabel }}</strong>
|
||||
<span v-if="order" class="settlement-invoice__meta">سفارش {{ order.orderNumber }}</span>
|
||||
</div>
|
||||
|
||||
<div class="settlement-mobile-options">
|
||||
<button
|
||||
type="button"
|
||||
class="settlement-card"
|
||||
:disabled="submitting"
|
||||
@click="selectType('cash')"
|
||||
>
|
||||
<span class="settlement-card__icon settlement-card__icon--cash">
|
||||
<IconCashSettlement :size="36" />
|
||||
</span>
|
||||
<h3>تسویه نقدی</h3>
|
||||
<p>پرداخت از طریق واریز به حساب شرکت</p>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="settlement-card"
|
||||
:disabled="submitting"
|
||||
@click="selectType('check')"
|
||||
>
|
||||
<span class="settlement-card__icon settlement-card__icon--check">
|
||||
<IconCheckBook :size="36" />
|
||||
</span>
|
||||
<h3>تسویه چکی</h3>
|
||||
<p>پرداخت بر اساس مبلغ فاکتور با ثبت چک</p>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -104,13 +121,26 @@ async function selectType(type: 'cash' | 'check') {
|
||||
<header class="desktop-page__head">
|
||||
<h1 class="desktop-page__title">انتخاب نوع تسویه</h1>
|
||||
<p class="desktop-page__subtitle">
|
||||
مبلغ فاکتور {{ invoiceLabel }}{{ order ? ` · سفارش ${order.orderNumber}` : '' }}
|
||||
<template v-if="invoiceReady">
|
||||
مبلغ فاکتور {{ invoiceLabel }}{{ order ? ` · سفارش ${order.orderNumber}` : '' }}
|
||||
</template>
|
||||
<template v-else>پس از قیمتگذاری مدیریت فعال میشود</template>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<div class="desktop-settlement-grid">
|
||||
<div v-if="!invoiceReady" class="settlement-waiting settlement-waiting--desktop">
|
||||
<h3 class="settlement-waiting__title">در انتظار قیمتگذاری</h3>
|
||||
<p class="settlement-waiting__text">
|
||||
پیشسفارش ثبت شده است. پس از صدور فاکتور میتوانید روش تسویه را انتخاب کنید.
|
||||
</p>
|
||||
<BaseButton variant="outline" @click="router.push(`/orders/${orderId}`)">
|
||||
بازگشت به جزئیات سفارش
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div v-else class="desktop-settlement-grid">
|
||||
<button
|
||||
type="button"
|
||||
class="settlement-card settlement-card--desktop"
|
||||
@@ -133,7 +163,7 @@ async function selectType(type: 'cash' | 'check') {
|
||||
<IconCheckBook :size="40" />
|
||||
</span>
|
||||
<h3>تسویه چکی</h3>
|
||||
<p>پرداخت از طریق ثبت چک</p>
|
||||
<p>پرداخت بر اساس مبلغ فاکتور با ثبت چک</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,6 +175,37 @@ async function selectType(type: 'cash' | 'check') {
|
||||
padding: 12px 0 0;
|
||||
}
|
||||
|
||||
.settlement-waiting {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 28px 20px;
|
||||
text-align: center;
|
||||
border-radius: 16px;
|
||||
background: var(--color-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.settlement-waiting--desktop {
|
||||
max-width: 480px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.settlement-waiting__title {
|
||||
margin: 0;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.settlement-waiting__text {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.7;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.settlement-mobile-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -169,7 +230,7 @@ async function selectType(type: 'cash' | 'check') {
|
||||
|
||||
.settlement-card:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: progress;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.settlement-invoice {
|
||||
@@ -203,10 +264,12 @@ async function selectType(type: 'cash' | 'check') {
|
||||
.settlement-card--desktop {
|
||||
min-height: 220px;
|
||||
border: 1px solid var(--color-border);
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.settlement-card--desktop:hover {
|
||||
.settlement-card--desktop:hover:not(:disabled) {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
@@ -202,7 +202,12 @@ const currentStep = 1
|
||||
gap: 20px;
|
||||
padding: 18px 22px;
|
||||
margin-bottom: 24px;
|
||||
background: linear-gradient(90deg, #fff 0%, #fff 58%, var(--color-primary-soft) 58%);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--color-surface) 0%,
|
||||
var(--color-surface) 58%,
|
||||
var(--color-primary-soft) 58%
|
||||
);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
@@ -222,8 +227,13 @@ const currentStep = 1
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
border-radius: 999px 999px 6px 6px;
|
||||
background: linear-gradient(180deg, #dbeafe 0%, #93c5fd 55%, #60a5fa 100%);
|
||||
box-shadow: inset 0 -4px 0 rgba(29, 53, 87, 0.08);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--color-primary) 22%, var(--color-surface)) 0%,
|
||||
color-mix(in srgb, var(--color-primary) 55%, var(--color-surface)) 55%,
|
||||
var(--color-primary) 100%
|
||||
);
|
||||
box-shadow: inset 0 -4px 0 color-mix(in srgb, var(--color-text) 8%, transparent);
|
||||
}
|
||||
|
||||
.welcome-banner__roll--1 {
|
||||
@@ -237,7 +247,12 @@ const currentStep = 1
|
||||
width: 30px;
|
||||
height: 44px;
|
||||
left: 44px;
|
||||
background: linear-gradient(180deg, #e2e8f0 0%, #94a3b8 55%, #64748b 100%);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--color-elevated) 0%,
|
||||
var(--color-border) 55%,
|
||||
var(--color-text-muted) 100%
|
||||
);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -267,7 +282,7 @@ const currentStep = 1
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
color: var(--color-surface);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -457,8 +472,8 @@ const currentStep = 1
|
||||
}
|
||||
|
||||
.quick-access__icon--orange {
|
||||
background: #fff4e5;
|
||||
color: #ea580c;
|
||||
background: var(--color-warning-light);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.quick-access__icon--blue {
|
||||
|
||||
@@ -28,6 +28,7 @@ import { getStatusHero, getTrackingFooterLabel } from '@/utils/orderTracking'
|
||||
import OrderTrackingTimeline from '@/components/orders/OrderTrackingTimeline.vue'
|
||||
import OrderStatusHero from '@/components/orders/OrderStatusHero.vue'
|
||||
import OrderTrackingSummary from '@/components/orders/OrderTrackingSummary.vue'
|
||||
import { isInvoicePriced } from '@/utils/orderInvoice'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -96,21 +97,6 @@ const trackingFooterLabel = computed(() =>
|
||||
order.value ? getTrackingFooterLabel(order.value.status) : 'مشاهده جزئیات سفارش',
|
||||
)
|
||||
|
||||
function formatOrderDateTime(date: string) {
|
||||
const d = new Date(date)
|
||||
const datePart = new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).format(d)
|
||||
const timePart = new Intl.DateTimeFormat('fa-IR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}).format(d)
|
||||
return `${timePart} - ${datePart}`
|
||||
}
|
||||
|
||||
function settlementLabel(type: 'cash' | 'check' | null) {
|
||||
if (!type) return 'در انتظار انتخاب مشتری'
|
||||
return type === 'cash' ? 'تسویه نقدی' : 'تسویه چکی'
|
||||
@@ -118,21 +104,27 @@ function settlementLabel(type: 'cash' | 'check' | null) {
|
||||
|
||||
type NextAction = { label: string; to?: string; receipt?: boolean }
|
||||
|
||||
/** اقدام بعدی که در توپ زمین مشتری است. */
|
||||
/** اقدام بعدی که در توپ زمین مشتری است؛ تسویه/چک فقط بعد از قیمتگذاری. */
|
||||
const nextAction = computed<NextAction | null>(() => {
|
||||
const current = order.value
|
||||
if (!current) return null
|
||||
const priced = isInvoicePriced(current)
|
||||
|
||||
switch (current.status) {
|
||||
case 'awaiting_settlement':
|
||||
case 'awaiting_settlement_fix':
|
||||
if (!priced) return null
|
||||
return { label: 'انتخاب روش تسویه', to: `/orders/${current.id}/settlement` }
|
||||
case 'check_in_progress':
|
||||
if (!priced) return null
|
||||
return { label: 'ثبت چکها', to: `/orders/${current.id}/check-mode` }
|
||||
case 'check_needs_fix':
|
||||
case 'check_rejected':
|
||||
if (!priced) return null
|
||||
return { label: 'اصلاح چکها', to: `/orders/${current.id}/check-list` }
|
||||
case 'awaiting_cash_payment':
|
||||
case 'cash_invoice_issued':
|
||||
if (!priced) return null
|
||||
return { label: 'ثبت رسید پرداخت', receipt: true }
|
||||
default:
|
||||
return null
|
||||
@@ -242,7 +234,7 @@ async function submitReceipt() {
|
||||
</header>
|
||||
|
||||
<div class="order-detail-page__body">
|
||||
<p v-if="showTracking" class="order-detail-page__order-id">{{ order.orderNumber }}</p>
|
||||
<p v-if="showTracking" class="order-detail-page__order-id mobile-only">{{ order.orderNumber }}</p>
|
||||
|
||||
<div v-if="!showTracking" class="order-info-card">
|
||||
<div class="order-info-card__head">
|
||||
@@ -258,7 +250,7 @@ async function submitReceipt() {
|
||||
<div class="order-info-card__row">
|
||||
<span class="order-info-card__key">تاریخ ثبت</span>
|
||||
<span class="order-info-card__value order-info-card__value--accent">
|
||||
{{ formatOrderDateTime(order.createdAt) }}
|
||||
{{ formatDateTime(order.createdAt) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -285,93 +277,111 @@ async function submitReceipt() {
|
||||
</div>
|
||||
|
||||
<div v-if="showTracking" class="order-detail-page__tracking">
|
||||
<button type="button" class="order-detail-back desktop-only" @click="router.push('/orders')">
|
||||
← بازگشت به سفارشها
|
||||
</button>
|
||||
|
||||
<OrderStatusHero v-if="statusHero" :config="statusHero" />
|
||||
|
||||
<div class="tracking-card">
|
||||
<h3 class="tracking-card__title">پیگیری وضعیت</h3>
|
||||
<OrderTrackingTimeline :order="order" />
|
||||
</div>
|
||||
|
||||
<OrderTrackingSummary :order="order" />
|
||||
|
||||
<template v-if="showOrderDetails">
|
||||
<section class="order-detail-page__tracking-main">
|
||||
<div class="tracking-card">
|
||||
<h3 class="tracking-card__title">اقلام سفارش</h3>
|
||||
<div v-for="item in order.items" :key="item.id" class="item-row">
|
||||
<div>
|
||||
<strong>{{ item.productName }}</strong>
|
||||
<p class="item-row__meta">کالیته {{ item.catalogCode }} · {{ item.color }}</p>
|
||||
</div>
|
||||
<span>{{ formatMeterage(item.requestedMeterage) }}</span>
|
||||
</div>
|
||||
<div class="item-row item-row--summary">
|
||||
<span>مجموع متراژ</span>
|
||||
<strong>{{ formatMeterage(order.totalMeterage) }}</strong>
|
||||
</div>
|
||||
<h3 class="tracking-card__title">پیگیری وضعیت</h3>
|
||||
<OrderTrackingTimeline :order="order" />
|
||||
</div>
|
||||
|
||||
<template v-if="order.settlementType === 'cash'">
|
||||
<div v-if="order.cashInvoiceExpiresAt" class="tracking-alert">
|
||||
اعتبار فاکتور: {{ formatDateTime(order.cashInvoiceExpiresAt) }}
|
||||
</div>
|
||||
|
||||
<template v-if="showOrderDetails">
|
||||
<div class="tracking-card">
|
||||
<h3 class="tracking-card__title">حسابهای بانکی</h3>
|
||||
<div v-for="acc in bankAccounts" :key="acc.id" class="bank-row">
|
||||
<strong>{{ acc.bankName }}</strong>
|
||||
<p dir="ltr" class="bank-row__meta">{{ acc.cardNumber }}</p>
|
||||
<p dir="ltr" class="bank-row__meta">{{ acc.iban }}</p>
|
||||
<h3 class="tracking-card__title">اقلام سفارش</h3>
|
||||
<div v-for="item in order.items" :key="item.id" class="item-row">
|
||||
<div>
|
||||
<strong>{{ item.productName }}</strong>
|
||||
<p class="item-row__meta">کالیته {{ item.catalogCode }} · {{ item.color }}</p>
|
||||
</div>
|
||||
<span>{{ formatMeterage(item.requestedMeterage) }}</span>
|
||||
</div>
|
||||
<div class="item-row item-row--summary">
|
||||
<span>مجموع متراژ</span>
|
||||
<strong>{{ formatMeterage(order.totalMeterage) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
v-if="nextAction?.receipt"
|
||||
block
|
||||
@click="showReceiptForm = true"
|
||||
>
|
||||
{{ order.cashReceipt ? 'ثبت رسید جدید' : 'ثبت رسید پرداخت' }}
|
||||
</BaseButton>
|
||||
<template v-if="order.settlementType === 'cash'">
|
||||
<div v-if="order.cashInvoiceExpiresAt" class="tracking-alert">
|
||||
اعتبار فاکتور: {{ formatDateTime(order.cashInvoiceExpiresAt) }}
|
||||
</div>
|
||||
|
||||
<div v-if="order.cashReceipt" class="tracking-card">
|
||||
<h3 class="tracking-card__title">رسید ثبتشده</h3>
|
||||
<div class="detail-row">
|
||||
<span>مبلغ</span>
|
||||
<span>{{ formatCurrency(order.cashReceipt.amount) }}</span>
|
||||
<div class="tracking-card">
|
||||
<h3 class="tracking-card__title">حسابهای بانکی</h3>
|
||||
<div v-for="acc in bankAccounts" :key="acc.id" class="bank-row">
|
||||
<strong>{{ acc.bankName }}</strong>
|
||||
<p dir="ltr" class="bank-row__meta">{{ acc.cardNumber }}</p>
|
||||
<p dir="ltr" class="bank-row__meta">{{ acc.iban }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span>شماره پیگیری</span>
|
||||
<span>{{ order.cashReceipt.trackingNumber }}</span>
|
||||
|
||||
<BaseButton
|
||||
v-if="nextAction?.receipt"
|
||||
block
|
||||
@click="showReceiptForm = true"
|
||||
>
|
||||
{{ order.cashReceipt ? 'ثبت رسید جدید' : 'ثبت رسید پرداخت' }}
|
||||
</BaseButton>
|
||||
|
||||
<div v-if="order.cashReceipt" class="tracking-card">
|
||||
<h3 class="tracking-card__title">رسید ثبتشده</h3>
|
||||
<div class="detail-row">
|
||||
<span>مبلغ</span>
|
||||
<span>{{ formatCurrency(order.cashReceipt.amount) }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span>شماره پیگیری</span>
|
||||
<span>{{ order.cashReceipt.trackingNumber }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="order.settlementType === 'check' && order.checks">
|
||||
<div class="tracking-card">
|
||||
<h3 class="tracking-card__title">رأس چکها</h3>
|
||||
<div class="rass-display">{{ order.checkRassDays }} روز</div>
|
||||
<p v-if="order.checkRassDate" class="rass-date">
|
||||
تاریخ رأس: {{ formatDate(order.checkRassDate) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-for="(check, i) in order.checks" :key="check.id" class="tracking-card">
|
||||
<strong>چک {{ i + 1 }}</strong>
|
||||
<div class="detail-row">
|
||||
<span>مبلغ</span>
|
||||
<span>{{ formatCurrency(check.amount) }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span>سررسید</span>
|
||||
<span>{{ formatDate(check.dueDate) }} ({{ daysUntilDue(check.dueDate) }} روز)</span>
|
||||
</div>
|
||||
<img v-if="check.frontImage" :src="check.frontImage" class="check-img" alt="چک" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<template v-if="order.settlementType === 'check' && order.checks">
|
||||
<div class="tracking-card">
|
||||
<h3 class="tracking-card__title">رأس چکها</h3>
|
||||
<div class="rass-display">{{ order.checkRassDays }} روز</div>
|
||||
<p v-if="order.checkRassDate" class="rass-date">
|
||||
تاریخ رأس: {{ formatDate(order.checkRassDate) }}
|
||||
</p>
|
||||
<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 v-for="(check, i) in order.checks" :key="check.id" class="tracking-card">
|
||||
<strong>چک {{ i + 1 }}</strong>
|
||||
<div class="detail-row">
|
||||
<span>مبلغ</span>
|
||||
<span>{{ formatCurrency(check.amount) }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span>سررسید</span>
|
||||
<span>{{ formatDate(check.dueDate) }} ({{ daysUntilDue(check.dueDate) }} روز)</span>
|
||||
</div>
|
||||
<img v-if="check.frontImage" :src="check.frontImage" class="check-img" alt="چک" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -383,15 +393,8 @@ async function submitReceipt() {
|
||||
<BaseButton block @click="openTracking">مشاهده جزئیات و پیگیری</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="order-detail-page__desktop-actions desktop-only">
|
||||
<BaseButton
|
||||
v-if="showTracking && !showOrderDetails"
|
||||
class="desktop-btn-compact"
|
||||
@click="openOrderDetails"
|
||||
>
|
||||
{{ trackingFooterLabel }}
|
||||
</BaseButton>
|
||||
<BaseButton v-if="!showTracking" class="desktop-btn-compact" @click="openTracking">
|
||||
<div v-if="!showTracking" class="order-detail-page__desktop-actions desktop-only">
|
||||
<BaseButton class="desktop-btn-compact" @click="openTracking">
|
||||
مشاهده جزئیات و پیگیری
|
||||
</BaseButton>
|
||||
</div>
|
||||
@@ -465,11 +468,13 @@ async function submitReceipt() {
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.order-detail-page {
|
||||
padding: 20px var(--page-padding-tablet) 28px;
|
||||
padding: 20px 0 28px;
|
||||
background: transparent;
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
max-width: none;
|
||||
margin-inline: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,6 +522,7 @@ async function submitReceipt() {
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.order-detail-page__order-id {
|
||||
@@ -643,8 +649,23 @@ async function submitReceipt() {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
margin-top: 8px;
|
||||
padding-bottom: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking-aside,
|
||||
.order-detail-page__tracking-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Mobile: status first, then timeline */
|
||||
.order-detail-page__tracking-aside {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.tracking-card {
|
||||
@@ -655,14 +676,87 @@ async function submitReceipt() {
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.tracking-card--hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking :deep(.status-hero) {
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking :deep(.tracking-summary) {
|
||||
margin: 0;
|
||||
padding: 12px 0 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border);
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.order-detail-page__tracking :deep(.status-hero) {
|
||||
align-items: flex-start;
|
||||
text-align: start;
|
||||
padding: 0 0 8px;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking :deep(.status-hero__desc) {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tracking-card__title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking :deep(.tracking-summary) {
|
||||
margin-top: 12px;
|
||||
.order-detail-page__tracking-cta {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.order-detail-page__desktop-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@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);
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking-aside {
|
||||
order: initial;
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.order-detail-page__desktop-actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.order-detail-page__footer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.order-detail-page__tracking {
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(300px, 0.85fr);
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.tracking-alert {
|
||||
|
||||
@@ -346,7 +346,7 @@ function handleSetting(item: (typeof settingsItems)[number]) {
|
||||
height: 92px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
color: var(--color-surface);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -363,7 +363,7 @@ function handleSetting(item: (typeof settingsItems)[number]) {
|
||||
padding: 9px 14px;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: var(--radius-md);
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-primary);
|
||||
font-family: inherit;
|
||||
font-size: 0.8125rem;
|
||||
@@ -413,7 +413,7 @@ function handleSetting(item: (typeof settingsItems)[number]) {
|
||||
}
|
||||
|
||||
.profile-overview__meta-icon {
|
||||
color: #94a3b8;
|
||||
color: var(--color-text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -466,8 +466,8 @@ function handleSetting(item: (typeof settingsItems)[number]) {
|
||||
}
|
||||
|
||||
.profile-stat__icon--amber {
|
||||
background: #fff6e5;
|
||||
color: #d97706;
|
||||
background: var(--color-warning-light);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.profile-stat__value {
|
||||
@@ -529,7 +529,7 @@ function handleSetting(item: (typeof settingsItems)[number]) {
|
||||
gap: 8px;
|
||||
margin: 0 0 14px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
@@ -550,7 +550,7 @@ function handleSetting(item: (typeof settingsItems)[number]) {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 11px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
@@ -598,14 +598,14 @@ function handleSetting(item: (typeof settingsItems)[number]) {
|
||||
padding: 16px 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: #f8fafc;
|
||||
background: var(--color-elevated);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.profile-quick-grid__item:hover {
|
||||
border-color: #c7d7ff;
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-soft);
|
||||
}
|
||||
|
||||
@@ -613,7 +613,7 @@ function handleSetting(item: (typeof settingsItems)[number]) {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -643,7 +643,7 @@ function handleSetting(item: (typeof settingsItems)[number]) {
|
||||
width: 100%;
|
||||
padding: 14px 4px;
|
||||
border: none;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
@@ -657,7 +657,7 @@ function handleSetting(item: (typeof settingsItems)[number]) {
|
||||
}
|
||||
|
||||
.profile-settings__item:hover {
|
||||
background: #f8fafc;
|
||||
background: var(--color-elevated);
|
||||
}
|
||||
|
||||
.profile-settings__icon {
|
||||
|
||||
Reference in New Issue
Block a user