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:
2026-08-09 14:32:10 +02:00
co-authored by Cursor
parent 7acd225fad
commit 9e455c2572
25 changed files with 652 additions and 351 deletions
+6 -31
View File
@@ -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;
+4 -28
View File
@@ -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 */
+1 -1
View File
@@ -5,7 +5,7 @@ defineProps<{
const steps = [
{ num: 1, label: 'سبد خرید' },
{ num: 2, label: 'ثبت سفارش' },
{ num: 2, label: 'پیش‌سفارش' },
{ num: 3, label: 'صدور فاکتور' },
{ num: 4, label: 'تسویه' },
]
+5 -2
View File
@@ -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 {
+28 -47
View File
@@ -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;
+1 -1
View File
@@ -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;
}
+36 -36
View File
@@ -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 {
+30 -9
View File
@@ -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);
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -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;
+42
View File
@@ -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 }
}
+2
View File
@@ -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>
+19
View File
@@ -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',
+43
View File
@@ -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
}
+20 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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>
+3 -4
View File
@@ -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 ?? '')
+3 -3
View File
@@ -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('')
+17 -1
View File
@@ -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);
+38 -5
View File
@@ -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 {
+75 -12
View File
@@ -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,6 +67,18 @@ async function selectType(type: 'cash' | 'check') {
<div class="page__scroll">
<FormErrorBanner :message="formError" />
<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>
<template v-else>
<div class="settlement-invoice">
<span class="settlement-invoice__label">مبلغ فاکتور</span>
<strong class="settlement-invoice__value">{{ invoiceLabel }}</strong>
@@ -92,9 +108,10 @@ async function selectType(type: 'cash' | 'check') {
<IconCheckBook :size="36" />
</span>
<h3>تسویه چکی</h3>
<p>پرداخت از طریق ثبت چک</p>
<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">
<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 {
+135 -41
View File
@@ -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,19 +277,12 @@ 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" />
<section class="order-detail-page__tracking-main">
<div class="tracking-card">
<h3 class="tracking-card__title">پیگیری وضعیت</h3>
<OrderTrackingTimeline :order="order" />
</div>
<OrderTrackingSummary :order="order" />
<template v-if="showOrderDetails">
<div class="tracking-card">
<h3 class="tracking-card__title">اقلام سفارش</h3>
@@ -372,6 +357,31 @@ async function submitReceipt() {
</div>
</template>
</template>
</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>
@@ -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 {