Initial commit: fabric-trade-user Vue 3 app with mobile and desktop layouts
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useOrderList } from '@/composables/useOrderList'
|
||||
import type { DesktopOrderFilterId } from '@/utils/orderStatusGroups'
|
||||
import MobileOrdersPage from '@/views/orders/components/MobileOrdersPage.vue'
|
||||
import DesktopOrdersPage from '@/views/orders/components/DesktopOrdersPage.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
tab,
|
||||
desktopFilter,
|
||||
stats,
|
||||
filteredOrdersMobile,
|
||||
filteredOrdersDesktop,
|
||||
paginatedOrders,
|
||||
totalPages,
|
||||
paginationRange,
|
||||
visiblePages,
|
||||
currentPage,
|
||||
companySlug,
|
||||
iconVariant,
|
||||
goToOrder,
|
||||
setPage,
|
||||
setDesktopFilter,
|
||||
viewStatFilter,
|
||||
} = useOrderList()
|
||||
|
||||
const mobileTabs = [
|
||||
{ id: 'all', label: 'همه' },
|
||||
{ id: 'active', label: 'در جریان' },
|
||||
{ id: 'done', label: 'تکمیل شده' },
|
||||
{ id: 'cancelled', label: 'لغو شده' },
|
||||
]
|
||||
|
||||
const desktopFilterTabs: { id: DesktopOrderFilterId; label: string }[] = [
|
||||
{ id: 'review', label: 'در انتظار بررسی' },
|
||||
{ id: 'in_progress', label: 'در حال انجام' },
|
||||
{ id: 'shipped', label: 'ارسال شده' },
|
||||
{ id: 'delivered', label: 'تحویل شده' },
|
||||
{ id: 'cancelled', label: 'لغو شده' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MobileOrdersPage
|
||||
class="mobile-only"
|
||||
:tab="tab"
|
||||
:tabs="mobileTabs"
|
||||
:orders="filteredOrdersMobile"
|
||||
:company-slug="companySlug"
|
||||
:icon-variant="iconVariant"
|
||||
@update:tab="tab = $event"
|
||||
@select="goToOrder"
|
||||
@back="router.push('/')"
|
||||
/>
|
||||
|
||||
<DesktopOrdersPage
|
||||
class="desktop-only"
|
||||
:stats="stats"
|
||||
:desktop-filter="desktopFilter"
|
||||
:desktop-filter-tabs="desktopFilterTabs"
|
||||
:filtered-orders="filteredOrdersDesktop"
|
||||
:paginated-orders="paginatedOrders"
|
||||
:pagination-range="paginationRange"
|
||||
:visible-pages="visiblePages"
|
||||
:current-page="currentPage"
|
||||
:total-pages="totalPages"
|
||||
@view-stat-filter="viewStatFilter"
|
||||
@set-desktop-filter="setDesktopFilter"
|
||||
@select="goToOrder"
|
||||
@set-page="setPage"
|
||||
@new-order="router.push('/')"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import BaseInput from '@/components/ui/BaseInput.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import { normalizePhone } from '@/utils/format'
|
||||
import {
|
||||
FORM_VALIDATION_SUMMARY,
|
||||
clearFieldError,
|
||||
hasFieldErrors,
|
||||
type FieldErrors,
|
||||
} from '@/utils/formErrors'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const phone = ref('')
|
||||
const fieldErrors = ref<FieldErrors>({})
|
||||
const formError = ref('')
|
||||
|
||||
function clearPhoneError() {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, 'phone')
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
fieldErrors.value = {}
|
||||
formError.value = ''
|
||||
|
||||
const normalized = normalizePhone(phone.value)
|
||||
const errors: FieldErrors = {}
|
||||
|
||||
if (!/^09\d{9}$/.test(normalized)) {
|
||||
errors.phone = 'شماره موبایل معتبر نیست.'
|
||||
}
|
||||
|
||||
if (hasFieldErrors(errors)) {
|
||||
fieldErrors.value = errors
|
||||
formError.value = FORM_VALIDATION_SUMMARY
|
||||
return
|
||||
}
|
||||
|
||||
const result = await auth.requestOtp(normalized, { isRegister: false })
|
||||
if (result.success) {
|
||||
router.push({ name: 'otp', query: { phone: normalized, mode: 'login' } })
|
||||
} else {
|
||||
formError.value = result.message ?? 'خطا در ارسال کد تأیید.'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page auth-page">
|
||||
<div class="page__scroll auth-page__scroll">
|
||||
<div class="auth-header">
|
||||
<div class="logo">🧵</div>
|
||||
<h1 class="page-title">ورود به سامانه</h1>
|
||||
<p class="page-subtitle">سامانه سفارش عمده پارچه</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<BaseInput
|
||||
v-model="phone"
|
||||
label="شماره موبایل"
|
||||
type="tel"
|
||||
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
|
||||
:error="fieldErrors.phone"
|
||||
hint="کد تأیید به این شماره ارسال میشود"
|
||||
@update:model-value="clearPhoneError"
|
||||
/>
|
||||
<BaseButton block :loading="auth.loading" @click="submit">دریافت کد تأیید</BaseButton>
|
||||
</div>
|
||||
|
||||
<p class="auth-footer text-center text-sm">
|
||||
حساب کاربری ندارید؟
|
||||
<router-link to="/register">ثبتنام</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.auth-page {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-page__scroll {
|
||||
padding-top: 36px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
margin-top: 24px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import BaseInput from '@/components/ui/BaseInput.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import {
|
||||
FORM_VALIDATION_SUMMARY,
|
||||
clearFieldError,
|
||||
hasFieldErrors,
|
||||
type FieldErrors,
|
||||
} from '@/utils/formErrors'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const phone = computed(() => (route.query.phone as string) ?? '')
|
||||
const code = ref('')
|
||||
const fieldErrors = ref<FieldErrors>({})
|
||||
const formError = ref('')
|
||||
|
||||
function clearCodeError() {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, 'code')
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
fieldErrors.value = {}
|
||||
formError.value = ''
|
||||
|
||||
const errors: FieldErrors = {}
|
||||
if (code.value.length < 4) {
|
||||
errors.code = 'کد تأیید را وارد کنید.'
|
||||
}
|
||||
|
||||
if (hasFieldErrors(errors)) {
|
||||
fieldErrors.value = errors
|
||||
formError.value = FORM_VALIDATION_SUMMARY
|
||||
return
|
||||
}
|
||||
|
||||
const result = await auth.confirmOtp(phone.value, code.value)
|
||||
if (result.success) {
|
||||
router.replace('/')
|
||||
} else {
|
||||
formError.value = result.message ?? 'کد تأیید نامعتبر است.'
|
||||
}
|
||||
}
|
||||
|
||||
async function resend() {
|
||||
const isRegister = route.query.mode === 'register'
|
||||
await auth.requestOtp(phone.value, { isRegister })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page auth-page">
|
||||
<div class="page__scroll auth-page__scroll">
|
||||
<div class="auth-header">
|
||||
<h1 class="page-title">تأیید شماره موبایل</h1>
|
||||
<p class="page-subtitle">کد ارسالشده به {{ phone }} را وارد کنید</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<BaseInput
|
||||
v-model="code"
|
||||
label="کد تأیید"
|
||||
type="text"
|
||||
placeholder="۱۲۳۴۵۶"
|
||||
maxlength="6"
|
||||
:error="fieldErrors.code"
|
||||
hint="در نسخه آزمایشی: 123456"
|
||||
@update:model-value="clearCodeError"
|
||||
/>
|
||||
<BaseButton block :loading="auth.loading" @click="submit">تأیید و ورود</BaseButton>
|
||||
<BaseButton block variant="ghost" class="mt-2" @click="resend">ارسال مجدد کد</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.auth-page {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-page__scroll {
|
||||
padding-top: 36px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import BaseInput from '@/components/ui/BaseInput.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import { normalizePhone } from '@/utils/format'
|
||||
import {
|
||||
FORM_VALIDATION_SUMMARY,
|
||||
clearFieldError,
|
||||
hasFieldErrors,
|
||||
type FieldErrors,
|
||||
} from '@/utils/formErrors'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const firstName = ref('')
|
||||
const lastName = ref('')
|
||||
const phone = ref('')
|
||||
const fieldErrors = ref<FieldErrors>({})
|
||||
const formError = ref('')
|
||||
|
||||
function clearField(key: string) {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, key)
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
fieldErrors.value = {}
|
||||
formError.value = ''
|
||||
|
||||
const normalized = normalizePhone(phone.value)
|
||||
const errors: FieldErrors = {}
|
||||
|
||||
if (!firstName.value.trim()) {
|
||||
errors.firstName = 'نام الزامی است.'
|
||||
}
|
||||
if (!lastName.value.trim()) {
|
||||
errors.lastName = 'نام خانوادگی الزامی است.'
|
||||
}
|
||||
if (!/^09\d{9}$/.test(normalized)) {
|
||||
errors.phone = 'شماره موبایل معتبر نیست.'
|
||||
}
|
||||
|
||||
if (hasFieldErrors(errors)) {
|
||||
fieldErrors.value = errors
|
||||
formError.value = FORM_VALIDATION_SUMMARY
|
||||
return
|
||||
}
|
||||
|
||||
const result = await auth.requestOtp(normalized, {
|
||||
isRegister: true,
|
||||
firstName: firstName.value.trim(),
|
||||
lastName: lastName.value.trim(),
|
||||
})
|
||||
if (result.success) {
|
||||
router.push({ name: 'otp', query: { phone: normalized, mode: 'register' } })
|
||||
} else {
|
||||
formError.value = result.message ?? 'خطا در ارسال کد تأیید.'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page auth-page">
|
||||
<div class="page__scroll auth-page__scroll">
|
||||
<div class="auth-header">
|
||||
<div class="logo">🧵</div>
|
||||
<h1 class="page-title">ثبتنام</h1>
|
||||
<p class="page-subtitle">ایجاد حساب کاربری جدید</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<BaseInput
|
||||
v-model="firstName"
|
||||
label="نام"
|
||||
placeholder="نام خود را وارد کنید"
|
||||
:error="fieldErrors.firstName"
|
||||
@update:model-value="clearField('firstName')"
|
||||
/>
|
||||
<BaseInput
|
||||
v-model="lastName"
|
||||
label="نام خانوادگی"
|
||||
placeholder="نام خانوادگی"
|
||||
:error="fieldErrors.lastName"
|
||||
@update:model-value="clearField('lastName')"
|
||||
/>
|
||||
<BaseInput
|
||||
v-model="phone"
|
||||
label="شماره موبایل"
|
||||
type="tel"
|
||||
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
|
||||
:error="fieldErrors.phone"
|
||||
@update:model-value="clearField('phone')"
|
||||
/>
|
||||
<BaseButton block :loading="auth.loading" @click="submit">دریافت کد تأیید</BaseButton>
|
||||
</div>
|
||||
|
||||
<p class="auth-footer text-center text-sm">
|
||||
قبلاً ثبتنام کردهاید؟
|
||||
<router-link to="/login">ورود</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.auth-page {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-page__scroll {
|
||||
padding-top: 36px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
margin-top: 24px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useCartPage } from '@/composables/useCartPage'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import MobileCartPage from '@/views/cart/components/MobileCartPage.vue'
|
||||
import DesktopCartPage from '@/views/cart/components/DesktopCartPage.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
cartStore,
|
||||
items,
|
||||
isEmpty,
|
||||
companyName,
|
||||
showFullDialog,
|
||||
removeItem,
|
||||
proceedToCheckout,
|
||||
proceedAnyway,
|
||||
createNewInvoice,
|
||||
} = useCartPage()
|
||||
|
||||
function goToOrdering() {
|
||||
const companyId = cartStore.activeCart?.companyId
|
||||
if (companyId) {
|
||||
router.push(`/companies/${companyId}/catalogs`)
|
||||
return
|
||||
}
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MobileCartPage
|
||||
class="mobile-only"
|
||||
:items="items"
|
||||
:company-name="companyName"
|
||||
:total-meterage="cartStore.totalMeterage"
|
||||
:max-meterage="cartStore.maxMeterage"
|
||||
:remaining-capacity="cartStore.remainingCapacity"
|
||||
:is-empty="isEmpty"
|
||||
@back="router.push('/')"
|
||||
@remove="removeItem"
|
||||
@checkout="proceedToCheckout"
|
||||
@select-company="router.push('/')"
|
||||
/>
|
||||
|
||||
<DesktopCartPage
|
||||
class="desktop-only"
|
||||
:items="items"
|
||||
:company-name="companyName"
|
||||
:total-meterage="cartStore.totalMeterage"
|
||||
:max-meterage="cartStore.maxMeterage"
|
||||
:remaining-capacity="cartStore.remainingCapacity"
|
||||
:is-empty="isEmpty"
|
||||
@remove="removeItem"
|
||||
@checkout="proceedToCheckout"
|
||||
@select-company="router.push('/')"
|
||||
@back="goToOrdering"
|
||||
/>
|
||||
|
||||
<div v-if="showFullDialog" class="dialog-overlay" @click.self="showFullDialog = false">
|
||||
<div class="dialog card">
|
||||
<h3>سقف متراژ تکمیل شد</h3>
|
||||
<p class="text-sm">
|
||||
سقف متراژ این فاکتور تکمیل شده است. آیا مایلید ادامه محصولات را در فاکتور جدید ثبت کنید؟
|
||||
</p>
|
||||
<div class="dialog-actions">
|
||||
<BaseButton variant="outline" @click="proceedAnyway">تسویه همین فاکتور</BaseButton>
|
||||
<BaseButton @click="createNewInvoice">فاکتور جدید</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dialog h3 {
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.dialog-actions {
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,608 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import CheckoutStepper from '@/components/layout/CheckoutStepper.vue'
|
||||
import IconCart from '@/components/icons/IconCart.vue'
|
||||
import IconTrash from '@/components/icons/IconTrash.vue'
|
||||
import IconBuilding from '@/components/icons/IconBuilding.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconOrders from '@/components/icons/IconOrders.vue'
|
||||
import IconClipboardEmpty from '@/components/icons/IconClipboardEmpty.vue'
|
||||
import IconLightbulb from '@/components/icons/IconLightbulb.vue'
|
||||
import IconLayoutGrid from '@/components/icons/IconLayoutGrid.vue'
|
||||
import IconDiamond from '@/components/icons/IconDiamond.vue'
|
||||
import { formatMeterage, formatNumber, formatCurrency } from '@/utils/format'
|
||||
import type { CartItem } from '@/types'
|
||||
|
||||
const props = defineProps<{
|
||||
items: CartItem[]
|
||||
companyName: string
|
||||
totalMeterage: number
|
||||
maxMeterage: number
|
||||
remainingCapacity: number
|
||||
isEmpty: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
remove: [id: string]
|
||||
checkout: []
|
||||
selectCompany: []
|
||||
back: []
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const payableAmount = computed(() =>
|
||||
props.items.reduce((sum, item) => sum + (item.unitPrice ?? 0) * item.meterage, 0),
|
||||
)
|
||||
|
||||
const quickLinks = [
|
||||
{
|
||||
title: 'انتخاب شرکت',
|
||||
desc: 'شروع سفارش از انتخاب شرکت',
|
||||
icon: IconBuilding,
|
||||
tone: 'blue',
|
||||
action: () => emit('selectCompany'),
|
||||
},
|
||||
{
|
||||
title: 'سفارشگذاری',
|
||||
desc: 'مشاهده کاتالوگ و افزودن کالا',
|
||||
icon: IconCart,
|
||||
tone: 'green',
|
||||
action: () => emit('back'),
|
||||
},
|
||||
{
|
||||
title: 'سفارشهای من',
|
||||
desc: 'پیگیری وضعیت سفارشها',
|
||||
icon: IconOrders,
|
||||
tone: 'blue',
|
||||
action: () => router.push('/orders'),
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-page cart-desktop-page">
|
||||
<CheckoutStepper :current-step="1" />
|
||||
|
||||
<header class="cart-desktop-page__head">
|
||||
<h1 class="cart-desktop-page__title">سبد خرید</h1>
|
||||
<p class="cart-desktop-page__subtitle">شرکت: {{ companyName || '—' }}</p>
|
||||
</header>
|
||||
|
||||
<div class="cart-desktop-page__layout">
|
||||
<section class="cart-desktop-page__main">
|
||||
<div v-if="isEmpty" class="cart-empty-card">
|
||||
<div class="cart-empty-card__visual" aria-hidden="true">
|
||||
<span class="cart-empty-card__spark cart-empty-card__spark--1">+</span>
|
||||
<span class="cart-empty-card__spark cart-empty-card__spark--2">·</span>
|
||||
<span class="cart-empty-card__circle">
|
||||
<IconCart :size="40" />
|
||||
</span>
|
||||
<span class="cart-empty-card__spark cart-empty-card__spark--3">·</span>
|
||||
</div>
|
||||
<h2 class="cart-empty-card__heading">سبد خرید شما خالی است</h2>
|
||||
<p class="cart-empty-card__text">
|
||||
برای ثبت سفارش جدید، ابتدا شرکت موردنظر را انتخاب کنید و سپس کالیته و محصولات
|
||||
را به سبد اضافه کنید.
|
||||
</p>
|
||||
<div class="cart-empty-card__actions">
|
||||
<button type="button" class="cart-btn cart-btn--primary" @click="emit('selectCompany')">
|
||||
<IconBuilding :size="18" />
|
||||
انتخاب شرکت
|
||||
</button>
|
||||
<button type="button" class="cart-btn cart-btn--outline" @click="emit('back')">
|
||||
<IconChevronLeft :size="16" />
|
||||
بازگشت به سفارشگذاری
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="cart-items-card">
|
||||
<div class="cart-items-card__scroll">
|
||||
<table class="cart-items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">محصول</th>
|
||||
<th scope="col">کد</th>
|
||||
<th scope="col">متراژ (متر)</th>
|
||||
<th scope="col">حذف</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in items" :key="item.id">
|
||||
<td>
|
||||
<strong class="cart-items-table__name">{{ item.productName }}</strong>
|
||||
</td>
|
||||
<td>{{ item.productCode }}</td>
|
||||
<td>{{ formatNumber(item.meterage) }}</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="cart-items-table__delete"
|
||||
aria-label="حذف"
|
||||
@click="emit('remove', item.id)"
|
||||
>
|
||||
<IconTrash :size="18" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="cart-desktop-page__sidebar">
|
||||
<div class="cart-summary-card">
|
||||
<h2 class="cart-summary-card__title">
|
||||
<span class="cart-summary-card__title-icon" aria-hidden="true">
|
||||
<IconClipboardEmpty :size="22" />
|
||||
</span>
|
||||
خلاصه سفارش
|
||||
</h2>
|
||||
<div class="cart-summary-card__rows">
|
||||
<div class="cart-summary-card__row">
|
||||
<span class="cart-summary-card__row-label">
|
||||
<IconLayoutGrid :size="16" />
|
||||
تعداد آیتمها
|
||||
</span>
|
||||
<strong>{{ formatNumber(items.length) }}</strong>
|
||||
</div>
|
||||
<div class="cart-summary-card__row">
|
||||
<span class="cart-summary-card__row-label">
|
||||
<IconDiamond :size="16" />
|
||||
متراژ کل
|
||||
</span>
|
||||
<strong>{{ formatMeterage(totalMeterage) }}</strong>
|
||||
</div>
|
||||
<div class="cart-summary-card__row">
|
||||
<span class="cart-summary-card__row-label">
|
||||
<IconOrders :size="16" />
|
||||
مبلغ قابل پرداخت
|
||||
</span>
|
||||
<strong>{{
|
||||
payableAmount > 0 ? formatCurrency(payableAmount) : '۰ تومان'
|
||||
}}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="cart-summary-card__cta"
|
||||
:disabled="isEmpty"
|
||||
@click="emit('checkout')"
|
||||
>
|
||||
<IconChevronLeft :size="16" />
|
||||
ادامه و انتخاب تسویه
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="cart-guide-card">
|
||||
<h3 class="cart-guide-card__title">
|
||||
<span class="cart-guide-card__title-icon" aria-hidden="true">
|
||||
<IconLightbulb :size="18" />
|
||||
</span>
|
||||
راهنمای گام بعدی
|
||||
</h3>
|
||||
<p class="cart-guide-card__text">
|
||||
پس از انتخاب شرکت و افزودن محصولات، متراژ کل و سقف مجاز فاکتور را بررسی کنید.
|
||||
<template v-if="!isEmpty">
|
||||
سقف مجاز: {{ formatMeterage(maxMeterage) }} — باقیمانده:
|
||||
{{ formatMeterage(remainingCapacity) }}.
|
||||
</template>
|
||||
سپس برای تکمیل سفارش، گزینه «ادامه و انتخاب تسویه» را بزنید.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<nav class="cart-quick-nav" aria-label="دسترسی سریع">
|
||||
<button
|
||||
v-for="link in quickLinks"
|
||||
:key="link.title"
|
||||
type="button"
|
||||
class="cart-quick-nav__item"
|
||||
@click="link.action()"
|
||||
>
|
||||
<span class="cart-quick-nav__icon" :class="`cart-quick-nav__icon--${link.tone}`">
|
||||
<component :is="link.icon" :size="20" />
|
||||
</span>
|
||||
<span class="cart-quick-nav__body">
|
||||
<strong class="cart-quick-nav__title">{{ link.title }}</strong>
|
||||
<span class="cart-quick-nav__desc">{{ link.desc }}</span>
|
||||
</span>
|
||||
<IconChevronLeft :size="16" class="cart-quick-nav__chevron" aria-hidden="true" />
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<p class="cart-desktop-page__footer">پارس بافت — سامانه سفارشگذاری پارچه</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cart-desktop-page {
|
||||
max-width: 980px;
|
||||
margin-inline: auto;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.cart-desktop-page__head {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cart-desktop-page__title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.cart-desktop-page__subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.cart-desktop-page__layout {
|
||||
direction: ltr;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.cart-desktop-page__main,
|
||||
.cart-desktop-page__sidebar {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.cart-desktop-page__sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.cart-empty-card,
|
||||
.cart-items-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.cart-empty-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 48px 32px 40px;
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.cart-empty-card__visual {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
height: 100px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.cart-empty-card__circle {
|
||||
position: absolute;
|
||||
inset: 12px 24px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cart-empty-card__spark {
|
||||
position: absolute;
|
||||
color: #93c5fd;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cart-empty-card__spark--1 {
|
||||
top: 8px;
|
||||
right: 18px;
|
||||
}
|
||||
|
||||
.cart-empty-card__spark--2 {
|
||||
top: 28px;
|
||||
left: 8px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.cart-empty-card__spark--3 {
|
||||
bottom: 16px;
|
||||
right: 10px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.cart-empty-card__heading {
|
||||
margin: 0 0 10px;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.cart-empty-card__text {
|
||||
margin: 0 0 28px;
|
||||
max-width: 380px;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.7;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.cart-empty-card__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cart-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 44px;
|
||||
padding: 0 22px;
|
||||
border-radius: var(--radius-md);
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.cart-btn--primary {
|
||||
border: none;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.cart-btn--primary:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.cart-btn--outline {
|
||||
border: 1px solid var(--color-border);
|
||||
background: #fff;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.cart-btn--outline:hover {
|
||||
background: var(--color-primary-soft);
|
||||
}
|
||||
|
||||
.cart-items-card__scroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.cart-items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
min-width: 520px;
|
||||
}
|
||||
|
||||
.cart-items-table th {
|
||||
padding: 14px 18px;
|
||||
text-align: start;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.cart-items-table td {
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
color: var(--color-text);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.cart-items-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cart-items-table__name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cart-items-table__delete {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
padding: 4px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.cart-items-table__delete:hover {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.cart-summary-card,
|
||||
.cart-guide-card {
|
||||
padding: 18px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.cart-summary-card__title,
|
||||
.cart-guide-card__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 14px;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.cart-summary-card__title-icon,
|
||||
.cart-guide-card__title-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.cart-summary-card__rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.cart-summary-card__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
font-size: 0.8125rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.cart-summary-card__row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cart-summary-card__row-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.cart-summary-card__row strong {
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.cart-summary-card__cta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.cart-summary-card__cta:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.cart-summary-card__cta:disabled {
|
||||
background: #e8ecf3;
|
||||
color: #94a3b8;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.cart-guide-card__text {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.75;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.cart-quick-nav {
|
||||
direction: ltr;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cart-quick-nav__item {
|
||||
direction: rtl;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--shadow-card);
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.cart-quick-nav__item:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.cart-quick-nav__icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cart-quick-nav__icon--blue {
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.cart-quick-nav__icon--green {
|
||||
background: var(--color-success-soft);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.cart-quick-nav__body {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.cart-quick-nav__title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.cart-quick-nav__desc {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.cart-quick-nav__chevron {
|
||||
color: var(--color-text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cart-desktop-page__footer {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,241 @@
|
||||
<script setup lang="ts">
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import IconCart from '@/components/icons/IconCart.vue'
|
||||
import IconTrash from '@/components/icons/IconTrash.vue'
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import { formatMeterage, formatNumber } from '@/utils/format'
|
||||
import type { CartItem } from '@/types'
|
||||
|
||||
defineProps<{
|
||||
items: CartItem[]
|
||||
companyName: string
|
||||
totalMeterage: number
|
||||
maxMeterage: number
|
||||
remainingCapacity: number
|
||||
isEmpty: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
back: []
|
||||
remove: [id: string]
|
||||
checkout: []
|
||||
selectCompany: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cart-mobile-page">
|
||||
<header class="cart-mobile-page__nav">
|
||||
<button type="button" class="nav-btn" aria-label="بازگشت" @click="emit('back')">
|
||||
<IconChevronBack :size="20" />
|
||||
</button>
|
||||
<h1 class="cart-mobile-page__title">سبد خرید</h1>
|
||||
<button type="button" class="nav-btn nav-btn--ghost" aria-hidden="true" tabindex="-1">
|
||||
<IconChevronBack :size="18" class="nav-btn__flip" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div v-if="!isEmpty" class="cart-mobile-page__body">
|
||||
<p class="cart-mobile-page__company">شرکت : {{ companyName }}</p>
|
||||
|
||||
<div class="cart-mobile-page__scroll">
|
||||
<div class="cart-mobile-table">
|
||||
<div class="cart-mobile-table__head">
|
||||
<span>محصول</span>
|
||||
<span>متراژ (متر)</span>
|
||||
<span>حذف</span>
|
||||
</div>
|
||||
<div v-for="item in items" :key="item.id" class="cart-mobile-table__row">
|
||||
<div class="col-product">
|
||||
<strong>{{ item.productName }}</strong>
|
||||
<span>کد: {{ item.productCode }}</span>
|
||||
</div>
|
||||
<div class="col-meterage">{{ formatNumber(item.meterage) }}</div>
|
||||
<div class="col-delete">
|
||||
<button type="button" aria-label="حذف" @click="emit('remove', item.id)">
|
||||
<IconTrash :size="20" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cart-mobile-page__footer">
|
||||
<div class="cart-mobile-summary">
|
||||
<div class="cart-mobile-summary__row">
|
||||
<span>جمع متراژ</span>
|
||||
<strong>{{ formatMeterage(totalMeterage) }}</strong>
|
||||
</div>
|
||||
<div class="cart-mobile-summary__row">
|
||||
<span>حداکثر مجاز</span>
|
||||
<strong>{{ formatMeterage(maxMeterage) }}</strong>
|
||||
</div>
|
||||
<div class="cart-mobile-summary__row">
|
||||
<span>باقی مانده</span>
|
||||
<strong>{{ formatMeterage(remainingCapacity) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<BaseButton block @click="emit('checkout')">ادامه و انتخاب نوع تسویه</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="cart-mobile-page__empty">
|
||||
<IconCart :size="56" />
|
||||
<p>سبد خرید خالی است</p>
|
||||
<BaseButton @click="emit('selectCompany')">انتخاب شرکت</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cart-mobile-page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: #fff;
|
||||
padding: 12px 0 0;
|
||||
}
|
||||
|
||||
.cart-mobile-page__nav {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 40px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cart-mobile-page__title {
|
||||
margin: 0;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-btn--ghost {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.nav-btn__flip {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.cart-mobile-page__body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.cart-mobile-page__company {
|
||||
margin: 0 0 16px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.cart-mobile-page__scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.cart-mobile-table {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cart-mobile-table__head,
|
||||
.cart-mobile-table__row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 88px 52px;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cart-mobile-table__head {
|
||||
background: #f1f5f9;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.cart-mobile-table__row {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.col-product strong {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.col-product span {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.col-meterage {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.col-delete {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.col-delete button {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.cart-mobile-page__footer {
|
||||
flex-shrink: 0;
|
||||
margin: 0 calc(-1 * var(--page-padding));
|
||||
padding: 12px var(--page-padding);
|
||||
border-top: 1px solid var(--color-border);
|
||||
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.06);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cart-mobile-summary {
|
||||
margin-bottom: 12px;
|
||||
padding: 14px 16px;
|
||||
background: #eef2f7;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.cart-mobile-summary__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.cart-mobile-summary__row:not(:last-child) {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.cart-mobile-page__empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,179 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useCatalogStore } from '@/stores/catalog'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import SearchField from '@/components/ui/SearchField.vue'
|
||||
import IconCatalog from '@/components/icons/IconCatalog.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const catalogStore = useCatalogStore()
|
||||
|
||||
const companyId = computed(() => route.params.companyId as string)
|
||||
const company = computed(() => catalogStore.getCompany(companyId.value))
|
||||
const search = ref('')
|
||||
|
||||
const catalogs = computed(() => catalogStore.fetchCatalogs(companyId.value, search.value))
|
||||
|
||||
function openCatalog(catalogId: string) {
|
||||
router.push(`/companies/${companyId.value}/catalogs/${catalogId}/products`)
|
||||
}
|
||||
|
||||
function getProductCount(catalogId: string) {
|
||||
return catalogStore.fetchProducts(catalogId).length
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Mobile -->
|
||||
<div class="page catalog-mobile-page mobile-only">
|
||||
<PageHeader
|
||||
:title="company?.name ?? 'کالیتهها'"
|
||||
subtitle="کالیته مورد نظر را انتخاب کنید"
|
||||
show-back
|
||||
centered
|
||||
@back="router.push('/')"
|
||||
/>
|
||||
|
||||
<SearchField
|
||||
v-model="search"
|
||||
placeholder="جستجو بر اساس کد یا نام کالیته..."
|
||||
class="catalog-page__search"
|
||||
/>
|
||||
|
||||
<div class="page__scroll page__scroll--with-nav">
|
||||
<div class="catalog-mobile-list">
|
||||
<button
|
||||
v-for="catalog in catalogs"
|
||||
:key="catalog.id"
|
||||
type="button"
|
||||
class="catalog-card"
|
||||
@click="openCatalog(catalog.id)"
|
||||
>
|
||||
<span class="catalog-card__icon"><IconCatalog :size="22" /></span>
|
||||
<div class="catalog-card__info">
|
||||
<h3>کالیته کد {{ catalog.code }}</h3>
|
||||
<p>{{ catalog.name }}</p>
|
||||
</div>
|
||||
<span class="catalog-card__count">{{ getProductCount(catalog.id) }} محصول</span>
|
||||
</button>
|
||||
<div v-if="catalogs.length === 0" class="catalog-empty">کالیتهای یافت نشد.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop -->
|
||||
<div class="desktop-page catalog-desktop-page desktop-only">
|
||||
<header class="desktop-page__head">
|
||||
<h1 class="desktop-page__title">{{ company?.name ?? 'کالیتهها' }}</h1>
|
||||
<p class="desktop-page__subtitle">کالیته مورد نظر را انتخاب کنید</p>
|
||||
</header>
|
||||
|
||||
<SearchField
|
||||
v-model="search"
|
||||
placeholder="جستجو بر اساس کد یا نام کالیته..."
|
||||
class="catalog-page__search"
|
||||
/>
|
||||
|
||||
<div class="desktop-grid-3">
|
||||
<button
|
||||
v-for="catalog in catalogs"
|
||||
:key="catalog.id"
|
||||
type="button"
|
||||
class="catalog-card catalog-card--desktop"
|
||||
@click="openCatalog(catalog.id)"
|
||||
>
|
||||
<span class="catalog-card__icon"><IconCatalog :size="24" /></span>
|
||||
<div class="catalog-card__info">
|
||||
<h3>کالیته کد {{ catalog.code }}</h3>
|
||||
<p>{{ catalog.name }}</p>
|
||||
</div>
|
||||
<span class="catalog-card__count">{{ getProductCount(catalog.id) }} محصول</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="catalogs.length === 0" class="catalog-empty">کالیتهای یافت نشد.</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.catalog-mobile-page {
|
||||
background: #fff;
|
||||
padding: 12px 0 0;
|
||||
}
|
||||
|
||||
.catalog-page__search {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.catalog-mobile-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.catalog-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--shadow-sm);
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.catalog-card--desktop {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-height: 140px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.catalog-card:hover {
|
||||
border-color: #cbd5e1;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.catalog-card__icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-primary-light);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.catalog-card__info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.catalog-card__info h3 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.catalog-card__info p {
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.catalog-card__count {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.catalog-empty {
|
||||
text-align: center;
|
||||
padding: 32px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,295 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import IconSuccessCircle from '@/components/icons/IconSuccessCircle.vue'
|
||||
import { toPersianDigits } from '@/utils/format'
|
||||
import { getCompanyById } from '@/data/mockData'
|
||||
import type { Order } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
const cartStore = useCartStore()
|
||||
const orderStore = useOrderStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const companyId = computed(() => cartStore.activeCart?.companyId ?? '')
|
||||
const companyName = computed(() => cartStore.activeCart?.companyName ?? '')
|
||||
const bankAccounts = computed(() => orderStore.getBankAccountsForCompany(companyId.value))
|
||||
|
||||
const expiryMessage = computed(() => {
|
||||
const hour = getCompanyById(companyId.value)?.cashInvoiceValidUntilHour ?? 17
|
||||
const time = toPersianDigits(String(hour).padStart(2, '0'))
|
||||
return `این فاکتور تا ساعت ${time}:۰۰ امروز معتبر است.`
|
||||
})
|
||||
|
||||
const accountsSectionTitle = computed(() => {
|
||||
const name = companyName.value.replace(/^شرکت\s+/u, '')
|
||||
return `شماره حسابهای شرکت ${name}`
|
||||
})
|
||||
|
||||
function formatCardNumber(value: string) {
|
||||
const digits = value.replace(/\D/g, '')
|
||||
return digits.replace(/(\d{4})(?=\d)/g, '$1 ').trim()
|
||||
}
|
||||
|
||||
function formatIban(value: string) {
|
||||
const clean = value.replace(/\s/g, '').toUpperCase()
|
||||
return clean.replace(/(.{4})/g, '$1 ').trim()
|
||||
}
|
||||
|
||||
function createOrder(): Order | null {
|
||||
if (!authStore.user || !cartStore.activeCart) return null
|
||||
const order = orderStore.submitOrder(authStore.user.id, cartStore.activeCart, 'cash')
|
||||
cartStore.clearActiveCart()
|
||||
return order
|
||||
}
|
||||
|
||||
function viewOrder() {
|
||||
const order = createOrder()
|
||||
if (!order) return
|
||||
router.replace(`/orders/${order.id}?track=1`)
|
||||
}
|
||||
|
||||
function registerReceipt() {
|
||||
const order = createOrder()
|
||||
if (!order) return
|
||||
router.replace(`/orders/${order.id}?track=1&receipt=1`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Mobile -->
|
||||
<div class="page cash-mobile-page mobile-only">
|
||||
<PageHeader
|
||||
title="تسویه نقدی"
|
||||
show-back
|
||||
centered
|
||||
@back="router.push('/checkout/settlement')"
|
||||
/>
|
||||
|
||||
<div class="cash-page__scroll page__scroll">
|
||||
<div class="cash-page__success">
|
||||
<IconSuccessCircle :size="56" />
|
||||
<p class="cash-page__success-title">فاکتور شما ثبت شد.</p>
|
||||
<p class="cash-page__success-subtitle">{{ expiryMessage }}</p>
|
||||
</div>
|
||||
|
||||
<section class="accounts-panel">
|
||||
<h2 class="accounts-panel__title">{{ accountsSectionTitle }}</h2>
|
||||
|
||||
<article v-for="account in bankAccounts" :key="account.id" class="bank-card">
|
||||
<div class="bank-card__head">
|
||||
<span class="bank-card__name">{{ account.bankName }}</span>
|
||||
<span class="bank-card__icon" aria-hidden="true">
|
||||
{{ account.bankName.charAt(0) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="bank-card__field">
|
||||
<span class="bank-card__label">شماره کارت</span>
|
||||
<strong class="bank-card__value" dir="ltr">{{ formatCardNumber(account.cardNumber) }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="bank-card__field">
|
||||
<span class="bank-card__label">شماره شبا</span>
|
||||
<strong class="bank-card__value" dir="ltr">{{ formatIban(account.iban) }}</strong>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="cash-page__footer">
|
||||
<BaseButton variant="outline" block @click="viewOrder">مشاهده سفارش</BaseButton>
|
||||
<BaseButton block class="cash-page__submit" @click="registerReceipt">ثبت رسید پرداخت</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop -->
|
||||
<div class="desktop-checkout-shell cash-desktop-page desktop-only">
|
||||
<header class="desktop-page__head">
|
||||
<h1 class="desktop-page__title">تسویه نقدی</h1>
|
||||
<p class="desktop-page__subtitle">{{ expiryMessage }}</p>
|
||||
</header>
|
||||
|
||||
<div class="cash-page__success">
|
||||
<IconSuccessCircle :size="56" />
|
||||
<p class="cash-page__success-title">فاکتور شما ثبت شد.</p>
|
||||
</div>
|
||||
|
||||
<section class="accounts-panel">
|
||||
<h2 class="accounts-panel__title">{{ accountsSectionTitle }}</h2>
|
||||
<div class="desktop-grid-2">
|
||||
<article v-for="account in bankAccounts" :key="account.id" class="bank-card">
|
||||
<div class="bank-card__head">
|
||||
<span class="bank-card__name">{{ account.bankName }}</span>
|
||||
<span class="bank-card__icon" aria-hidden="true">{{ account.bankName.charAt(0) }}</span>
|
||||
</div>
|
||||
<div class="bank-card__field">
|
||||
<span class="bank-card__label">شماره کارت</span>
|
||||
<strong class="bank-card__value" dir="ltr">{{ formatCardNumber(account.cardNumber) }}</strong>
|
||||
</div>
|
||||
<div class="bank-card__field">
|
||||
<span class="bank-card__label">شماره شبا</span>
|
||||
<strong class="bank-card__value" dir="ltr">{{ formatIban(account.iban) }}</strong>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="cash-desktop-page__actions">
|
||||
<BaseButton variant="outline" class="desktop-btn-compact" @click="viewOrder">مشاهده سفارش</BaseButton>
|
||||
<BaseButton class="desktop-btn-compact" @click="registerReceipt">ثبت رسید پرداخت</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cash-mobile-page {
|
||||
overflow: hidden;
|
||||
background: var(--color-bg);
|
||||
padding: 12px 0 0;
|
||||
}
|
||||
|
||||
.cash-desktop-page__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.cash-page__scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.cash-page__success {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.cash-page__success-title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.cash-page__success-subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.accounts-panel {
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
background: #eef2f7;
|
||||
}
|
||||
|
||||
.accounts-panel__title {
|
||||
margin: 0 0 14px;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.bank-card {
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.bank-card + .bank-card {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.cash-mobile-page .bank-card + .bank-card {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.cash-desktop-page .bank-card + .bank-card {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.bank-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.bank-card__icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bank-card__name {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.bank-card__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.bank-card__field + .bank-card__field {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.bank-card__label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.bank-card__value {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.cash-page__footer {
|
||||
flex-shrink: 0;
|
||||
padding: 12px 0 calc(12px + env(safe-area-inset-bottom, 0));
|
||||
background: var(--color-bg);
|
||||
border-top: 1px solid var(--color-border);
|
||||
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.06);
|
||||
margin: 0 calc(-1 * var(--page-padding));
|
||||
padding-left: var(--page-padding);
|
||||
padding-right: var(--page-padding);
|
||||
}
|
||||
|
||||
.cash-page__submit {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,317 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import BaseInput from '@/components/ui/BaseInput.vue'
|
||||
import CurrencyInput from '@/components/ui/CurrencyInput.vue'
|
||||
import PersianDateInput from '@/components/ui/PersianDateInput.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import { parseFormattedNumber } from '@/utils/format'
|
||||
import {
|
||||
FORM_VALIDATION_SUMMARY,
|
||||
clearFieldError,
|
||||
hasFieldErrors,
|
||||
type FieldErrors,
|
||||
} from '@/utils/formErrors'
|
||||
import {
|
||||
validateNationalId,
|
||||
validateSayadId,
|
||||
validateImageFile,
|
||||
fileToBase64,
|
||||
} from '@/utils/validation'
|
||||
|
||||
const router = useRouter()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const isFull = computed(() => orderStore.draftCheckMode === 'full')
|
||||
|
||||
const amount = ref('')
|
||||
const dueDate = ref('')
|
||||
const sayadId = ref('')
|
||||
const checkNumber = ref('')
|
||||
const ownerNationalId = ref('')
|
||||
const ownerFirstName = ref('')
|
||||
const ownerLastName = ref('')
|
||||
const bankName = ref('')
|
||||
const branchName = ref('')
|
||||
const frontImage = ref('')
|
||||
const backImage = ref('')
|
||||
const notes = ref('')
|
||||
const fieldErrors = ref<FieldErrors>({})
|
||||
const formError = ref('')
|
||||
|
||||
watch(amount, () => {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, 'amount')
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
})
|
||||
watch(dueDate, () => {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, 'dueDate')
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
})
|
||||
watch(sayadId, () => {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, 'sayadId')
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
})
|
||||
watch(ownerNationalId, () => {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, 'ownerNationalId')
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
})
|
||||
watch(frontImage, () => {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, 'frontImage')
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
})
|
||||
|
||||
async function handleImageUpload(event: Event, target: 'front' | 'back') {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
const validation = validateImageFile(file)
|
||||
if (!validation.valid) {
|
||||
if (target === 'front') {
|
||||
fieldErrors.value = { ...fieldErrors.value, frontImage: validation.message ?? 'خطا' }
|
||||
}
|
||||
return
|
||||
}
|
||||
const base64 = await fileToBase64(file)
|
||||
if (target === 'front') frontImage.value = base64
|
||||
else backImage.value = base64
|
||||
}
|
||||
|
||||
function validateForm(): boolean {
|
||||
fieldErrors.value = {}
|
||||
formError.value = ''
|
||||
|
||||
const errors: FieldErrors = {}
|
||||
const amountNum = parseFormattedNumber(amount.value)
|
||||
|
||||
if (!amountNum || amountNum <= 0) {
|
||||
errors.amount = 'مبلغ چک باید بزرگتر از صفر باشد.'
|
||||
}
|
||||
if (!dueDate.value) {
|
||||
errors.dueDate = 'تاریخ سررسید الزامی است.'
|
||||
}
|
||||
if (!frontImage.value) {
|
||||
errors.frontImage = 'تصویر روی چک الزامی است.'
|
||||
}
|
||||
if (isFull.value) {
|
||||
if (!validateSayadId(sayadId.value)) {
|
||||
errors.sayadId = 'شناسه صیادی باید ۱۶ رقم باشد.'
|
||||
}
|
||||
if (!validateNationalId(ownerNationalId.value)) {
|
||||
errors.ownerNationalId = 'کد ملی صاحب چک معتبر نیست.'
|
||||
}
|
||||
const existingSayad = orderStore.draftChecks.some(
|
||||
(c) => c.sayadId === sayadId.value.replace(/\s/g, ''),
|
||||
)
|
||||
if (existingSayad) {
|
||||
errors.sayadId = 'شناسه صیادی تکراری است.'
|
||||
}
|
||||
}
|
||||
|
||||
fieldErrors.value = errors
|
||||
if (hasFieldErrors(errors)) {
|
||||
formError.value = FORM_VALIDATION_SUMMARY
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function saveCheck() {
|
||||
if (!validateForm()) return
|
||||
|
||||
orderStore.addDraftCheck({
|
||||
amount: parseFormattedNumber(amount.value),
|
||||
dueDate: dueDate.value,
|
||||
sayadId: isFull.value ? sayadId.value.replace(/\s/g, '') : undefined,
|
||||
checkNumber: isFull.value ? checkNumber.value : undefined,
|
||||
ownerNationalId: isFull.value ? ownerNationalId.value : undefined,
|
||||
ownerFirstName: isFull.value ? ownerFirstName.value : undefined,
|
||||
ownerLastName: isFull.value ? ownerLastName.value : undefined,
|
||||
bankName: isFull.value ? bankName.value : undefined,
|
||||
branchName: isFull.value ? branchName.value : undefined,
|
||||
frontImage: frontImage.value,
|
||||
backImage: backImage.value || undefined,
|
||||
notes: notes.value || undefined,
|
||||
mode: orderStore.draftCheckMode ?? 'brief',
|
||||
})
|
||||
|
||||
resetForm()
|
||||
router.push('/checkout/check-list')
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
amount.value = ''
|
||||
dueDate.value = ''
|
||||
sayadId.value = ''
|
||||
checkNumber.value = ''
|
||||
ownerNationalId.value = ''
|
||||
ownerFirstName.value = ''
|
||||
ownerLastName.value = ''
|
||||
bankName.value = ''
|
||||
branchName.value = ''
|
||||
frontImage.value = ''
|
||||
backImage.value = ''
|
||||
notes.value = ''
|
||||
fieldErrors.value = {}
|
||||
formError.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<PageHeader
|
||||
:title="isFull ? 'ثبت چک - حالت کامل' : 'ثبت چک - حالت مختصر'"
|
||||
show-back
|
||||
@back="router.push('/checkout/check-mode')"
|
||||
/>
|
||||
|
||||
<div class="page__scroll checkout-page__scroll">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<div class="steps">
|
||||
<span class="step active">۱. اطلاعات چک</span>
|
||||
<span class="step">۲. چک بعدی</span>
|
||||
<span class="step">۳. خلاصه</span>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<CurrencyInput
|
||||
v-model="amount"
|
||||
label="مبلغ چک (تومان)"
|
||||
placeholder="مبلغ"
|
||||
:error="fieldErrors.amount"
|
||||
/>
|
||||
<PersianDateInput v-model="dueDate" label="تاریخ سررسید" :error="fieldErrors.dueDate" />
|
||||
|
||||
<template v-if="isFull">
|
||||
<BaseInput
|
||||
v-model="sayadId"
|
||||
label="شناسه صیادی"
|
||||
placeholder="۱۶ رقم"
|
||||
maxlength="16"
|
||||
:error="fieldErrors.sayadId"
|
||||
/>
|
||||
<BaseInput v-model="checkNumber" label="شماره چک" />
|
||||
<BaseInput
|
||||
v-model="ownerNationalId"
|
||||
label="کد ملی صاحب چک"
|
||||
maxlength="10"
|
||||
:error="fieldErrors.ownerNationalId"
|
||||
/>
|
||||
<BaseInput v-model="ownerFirstName" label="نام صاحب چک" />
|
||||
<BaseInput v-model="ownerLastName" label="نام خانوادگی صاحب چک" />
|
||||
<BaseInput v-model="bankName" label="نام بانک" />
|
||||
<BaseInput v-model="branchName" label="نام شعبه" />
|
||||
</template>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">تصویر روی چک *</label>
|
||||
<label class="upload-box" :class="{ 'upload-box--error': fieldErrors.frontImage }">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="upload-box__input"
|
||||
@change="handleImageUpload($event, 'front')"
|
||||
/>
|
||||
<img v-if="frontImage" :src="frontImage" class="upload-box__preview" alt="روی چک" />
|
||||
<template v-else>
|
||||
<span class="upload-box__placeholder">بارگذاری تصویر چک</span>
|
||||
</template>
|
||||
</label>
|
||||
<p v-if="fieldErrors.frontImage" class="input-error-text">{{ fieldErrors.frontImage }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="isFull" class="form-group">
|
||||
<label class="form-label">تصویر پشت چک</label>
|
||||
<label class="upload-box">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="upload-box__input"
|
||||
@change="handleImageUpload($event, 'back')"
|
||||
/>
|
||||
<img v-if="backImage" :src="backImage" class="upload-box__preview" alt="پشت چک" />
|
||||
<template v-else>
|
||||
<span class="upload-box__placeholder">بارگذاری تصویر</span>
|
||||
</template>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<BaseInput v-model="notes" label="توضیحات" />
|
||||
|
||||
<BaseButton block class="mt-4" @click="saveCheck">ذخیره چک</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.checkout-page__scroll {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.steps {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
|
||||
.step {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 10px 6px;
|
||||
background: var(--color-tab-inactive-bg);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.step.active {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 120px;
|
||||
border: 1.5px dashed var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: #fafafa;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.upload-box--error {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.upload-box__input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-box__placeholder {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.upload-box__preview {
|
||||
max-width: 100%;
|
||||
max-height: 140px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.input-error-text {
|
||||
color: var(--color-danger);
|
||||
font-size: 0.8125rem;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,549 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import EmptyState from '@/components/ui/EmptyState.vue'
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import { formatCurrency, formatDate, formatNumber, toPersianDigits } from '@/utils/format'
|
||||
import { calculateCheckRass } from '@/utils/checkRass'
|
||||
import type { CheckData } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const cartStore = useCartStore()
|
||||
const orderStore = useOrderStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const isCheckout = computed(() => route.name === 'check-list')
|
||||
|
||||
const checks = computed<CheckData[]>(() => {
|
||||
if (orderStore.draftChecks.length > 0) return orderStore.draftChecks
|
||||
if (!authStore.user) return []
|
||||
|
||||
return orderStore
|
||||
.userOrders(authStore.user.id)
|
||||
.filter((order) => order.settlementType === 'check' && order.checks?.length)
|
||||
.flatMap((order) => order.checks ?? [])
|
||||
})
|
||||
|
||||
const rass = computed(() => {
|
||||
if (orderStore.draftChecks.length > 0) return orderStore.getDraftRass()
|
||||
if (checks.value.length === 0) {
|
||||
return calculateCheckRass([], new Date())
|
||||
}
|
||||
return calculateCheckRass(checks.value, new Date())
|
||||
})
|
||||
|
||||
const invoiceAmount = computed(() => {
|
||||
if (!authStore.user) return 0
|
||||
|
||||
const activeOrder = orderStore
|
||||
.userOrders(authStore.user.id)
|
||||
.find((order) => order.settlementType === 'check' && order.finalAmount)
|
||||
|
||||
if (activeOrder?.finalAmount) return activeOrder.finalAmount
|
||||
if (isCheckout.value && rass.value.totalAmount > 0) return rass.value.totalAmount
|
||||
return rass.value.totalAmount
|
||||
})
|
||||
|
||||
const isAmountComplete = computed(
|
||||
() => invoiceAmount.value > 0 && rass.value.totalAmount >= invoiceAmount.value,
|
||||
)
|
||||
|
||||
const openMenuId = computed({
|
||||
get: () => (route.query.menu as string) ?? '',
|
||||
set: (id: string) => {
|
||||
router.replace({ query: id ? { menu: id } : {} })
|
||||
},
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
router.push(isCheckout.value ? '/checkout/check-form' : '/profile')
|
||||
}
|
||||
|
||||
function addAnother() {
|
||||
router.push('/checkout/check-form')
|
||||
}
|
||||
|
||||
function removeCheck(id: string) {
|
||||
orderStore.removeDraftCheck(id)
|
||||
openMenuId.value = ''
|
||||
}
|
||||
|
||||
function toggleMenu(id: string) {
|
||||
openMenuId.value = openMenuId.value === id ? '' : id
|
||||
}
|
||||
|
||||
function submitOrder() {
|
||||
if (!authStore.user || !cartStore.activeCart || checks.value.length === 0) return
|
||||
|
||||
const order = orderStore.submitOrder(
|
||||
authStore.user.id,
|
||||
cartStore.activeCart,
|
||||
'check',
|
||||
[...orderStore.draftChecks],
|
||||
orderStore.draftCheckMode ?? 'brief',
|
||||
)
|
||||
cartStore.clearActiveCart()
|
||||
router.replace(`/orders/${order.id}/success`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Mobile -->
|
||||
<div class="check-list-page mobile-only">
|
||||
<header class="check-list-page__nav">
|
||||
<button type="button" class="nav-btn" aria-label="بازگشت" @click="goBack">
|
||||
<IconChevronBack :size="20" />
|
||||
</button>
|
||||
<h1 class="check-list-page__title">چکهای ثبت شده</h1>
|
||||
<button type="button" class="nav-btn nav-btn--ghost" aria-hidden="true" tabindex="-1">
|
||||
<IconChevronBack :size="18" class="nav-btn__flip" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div v-if="checks.length === 0" class="check-list-page__empty page__scroll page__scroll--with-nav">
|
||||
<p>چکی ثبت نشده است.</p>
|
||||
<BaseButton v-if="isCheckout" @click="addAnother">ثبت چک جدید</BaseButton>
|
||||
<BaseButton v-else variant="outline" @click="router.push('/')">ثبت سفارش جدید</BaseButton>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="check-list-page__scroll page__scroll">
|
||||
<div class="summary-cards">
|
||||
<div class="summary-card">
|
||||
<span class="summary-card__label">مجموع مبلغ چکها</span>
|
||||
<strong class="summary-card__value">{{ formatNumber(rass.totalAmount) }}</strong>
|
||||
<span class="summary-card__unit">تومان</span>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-card__label">مبلغ فاکتور</span>
|
||||
<strong class="summary-card__value">{{ formatNumber(invoiceAmount) }}</strong>
|
||||
<span class="summary-card__unit">تومان</span>
|
||||
<p v-if="isAmountComplete" class="summary-card__status">
|
||||
<span class="summary-card__status-icon">✓</span>
|
||||
مبلغ کامل است
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checks-list">
|
||||
<article v-for="(check, index) in checks" :key="check.id" class="check-card">
|
||||
<div class="check-card__menu-wrap">
|
||||
<button
|
||||
v-if="isCheckout && orderStore.draftChecks.length > 0"
|
||||
type="button"
|
||||
class="check-card__menu-btn"
|
||||
aria-label="گزینهها"
|
||||
@click="toggleMenu(check.id)"
|
||||
>
|
||||
⋮
|
||||
</button>
|
||||
<div v-if="openMenuId === check.id" class="check-card__menu">
|
||||
<button type="button" @click="removeCheck(check.id)">حذف</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="check-card__body">
|
||||
<h3 class="check-card__title">چک {{ toPersianDigits(String(index + 1)) }}</h3>
|
||||
<p class="check-card__amount">{{ formatCurrency(check.amount) }}</p>
|
||||
<p class="check-card__date">تاریخ: {{ formatDate(check.dueDate) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="check-card__thumb-wrap">
|
||||
<img
|
||||
v-if="check.frontImage"
|
||||
:src="check.frontImage"
|
||||
class="check-card__thumb"
|
||||
alt="تصویر چک"
|
||||
/>
|
||||
<div v-else class="check-card__thumb check-card__thumb--empty">چک</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="rass-panel">
|
||||
<h3 class="rass-panel__title">رأس چکها</h3>
|
||||
<div class="rass-panel__days">{{ toPersianDigits(String(rass.rassDays)) }} روز</div>
|
||||
<p v-if="rass.rassDate" class="rass-panel__date">
|
||||
تاریخ رأس تقریبی: {{ formatDate(rass.rassDate) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isCheckout && orderStore.draftChecks.length > 0" class="check-list-page__footer">
|
||||
<BaseButton block :disabled="checks.length === 0" @click="submitOrder">
|
||||
ارسال برای بررسی
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Desktop -->
|
||||
<div v-if="checks.length > 0" class="desktop-checkout-shell check-list-desktop desktop-only">
|
||||
<header class="desktop-page__head">
|
||||
<h1 class="desktop-page__title">چکهای ثبت شده</h1>
|
||||
<p class="desktop-page__subtitle">خلاصه و فهرست چکهای ثبتشده</p>
|
||||
</header>
|
||||
|
||||
<div class="desktop-two-col">
|
||||
<div class="desktop-two-col__main">
|
||||
<div class="desktop-table-wrap">
|
||||
<table class="desktop-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">تصویر</th>
|
||||
<th scope="col">مبلغ (تومان)</th>
|
||||
<th scope="col">تاریخ سررسید</th>
|
||||
<th scope="col">شناسه</th>
|
||||
<th v-if="isCheckout" scope="col">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(check, index) in checks" :key="check.id">
|
||||
<td>
|
||||
<img
|
||||
v-if="check.frontImage"
|
||||
:src="check.frontImage"
|
||||
class="check-list-desktop__thumb"
|
||||
alt="تصویر چک"
|
||||
/>
|
||||
<span v-else>—</span>
|
||||
</td>
|
||||
<td>{{ formatNumber(check.amount) }}</td>
|
||||
<td>{{ formatDate(check.dueDate) }}</td>
|
||||
<td>چک {{ toPersianDigits(String(index + 1)) }}</td>
|
||||
<td v-if="isCheckout && orderStore.draftChecks.length > 0">
|
||||
<button type="button" class="orders-table__action" @click="removeCheck(check.id)">
|
||||
حذف
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<BaseButton
|
||||
v-if="isCheckout"
|
||||
variant="outline"
|
||||
class="desktop-btn-compact check-list-desktop__add"
|
||||
@click="addAnother"
|
||||
>
|
||||
ثبت چک دیگر
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<aside class="desktop-summary-panel">
|
||||
<h2 class="desktop-summary-panel__title">خلاصه مالی</h2>
|
||||
<div class="desktop-summary-panel__row">
|
||||
<span>مجموع چکها</span>
|
||||
<strong>{{ formatNumber(rass.totalAmount) }} تومان</strong>
|
||||
</div>
|
||||
<div class="desktop-summary-panel__row">
|
||||
<span>مبلغ فاکتور</span>
|
||||
<strong>{{ formatNumber(invoiceAmount) }} تومان</strong>
|
||||
</div>
|
||||
<div class="desktop-summary-panel__row">
|
||||
<span>رأس چکها</span>
|
||||
<strong>{{ toPersianDigits(String(rass.rassDays)) }} روز</strong>
|
||||
</div>
|
||||
<div v-if="isCheckout && orderStore.draftChecks.length > 0" class="desktop-summary-panel__action">
|
||||
<BaseButton block :disabled="checks.length === 0" @click="submitOrder">
|
||||
ارسال برای بررسی
|
||||
</BaseButton>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="desktop-checkout-shell desktop-only">
|
||||
<EmptyState message="چکی ثبت نشده است">
|
||||
<BaseButton v-if="isCheckout" @click="addAnother">ثبت چک جدید</BaseButton>
|
||||
</EmptyState>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.check-list-desktop__thumb {
|
||||
width: 48px;
|
||||
height: 32px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.check-list-desktop__add {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.check-list-page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
padding: 12px var(--page-padding) 0;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.check-list-page__nav {
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 40px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.check-list-page__title {
|
||||
margin: 0;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-btn--ghost {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-btn__flip {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.check-list-page__scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.check-list-page__empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.summary-cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
padding: 14px 12px;
|
||||
border-radius: 14px;
|
||||
background: #f1f5f9;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.summary-card__label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.summary-card__value {
|
||||
display: block;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.summary-card__unit {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.summary-card__status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
margin: 8px 0 0;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.summary-card__status-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-success-light);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
.checks-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.check-card {
|
||||
display: grid;
|
||||
grid-template-columns: 28px 1fr 72px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #eef2f7;
|
||||
}
|
||||
|
||||
.check-card__menu-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.check-card__menu-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 1.125rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.check-card__menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
min-width: 88px;
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: #fff;
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.check-card__menu button {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 8px 10px;
|
||||
text-align: right;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-danger);
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.check-card__menu button:hover {
|
||||
background: var(--color-danger-light);
|
||||
}
|
||||
|
||||
.check-card__body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.check-card__title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.check-card__amount {
|
||||
margin: 0 0 4px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.check-card__date {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.check-card__thumb-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.check-card__thumb {
|
||||
width: 72px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--color-border);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.check-card__thumb--empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.rass-panel {
|
||||
padding: 16px;
|
||||
border-radius: 14px;
|
||||
background: #ecfdf3;
|
||||
border: 1px solid #bbf7d0;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rass-panel__title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.rass-panel__days {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: #15803d;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.rass-panel__date {
|
||||
margin: 6px 0 0;
|
||||
font-size: 0.8125rem;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.check-list-page__footer {
|
||||
flex-shrink: 0;
|
||||
padding: 12px 0 calc(12px + env(safe-area-inset-bottom, 0));
|
||||
background: #fff;
|
||||
border-top: 1px solid var(--color-border);
|
||||
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.06);
|
||||
margin: 0 calc(-1 * var(--page-padding));
|
||||
padding-left: var(--page-padding);
|
||||
padding-right: var(--page-padding);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
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'
|
||||
|
||||
const router = useRouter()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
function selectMode(mode: 'full' | 'brief') {
|
||||
orderStore.setCheckMode(mode)
|
||||
router.push('/checkout/check-form')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page check-mode-page">
|
||||
<PageHeader
|
||||
title="تسویه چکی"
|
||||
subtitle="نحوه ثبت اطلاعات چکها را انتخاب کنید"
|
||||
show-back
|
||||
centered
|
||||
@back="router.push('/checkout/settlement')"
|
||||
/>
|
||||
|
||||
<div class="page__scroll check-mode-page__scroll">
|
||||
<div class="mode-options">
|
||||
<button type="button" class="mode-card" @click="selectMode('full')">
|
||||
<span class="mode-card__icon">
|
||||
<IconCheckBook :size="36" />
|
||||
</span>
|
||||
<h3 class="mode-card__title">ثبت کامل (پیشنهادی)</h3>
|
||||
<p class="mode-card__desc">وارد کردن همه اطلاعات چک</p>
|
||||
</button>
|
||||
|
||||
<button type="button" class="mode-card" @click="selectMode('brief')">
|
||||
<span class="mode-card__icon">
|
||||
<IconEditPen :size="36" />
|
||||
</span>
|
||||
<h3 class="mode-card__title">ثبت مختصر</h3>
|
||||
<p class="mode-card__desc">وارد کردن حداقل اطلاعات</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.check-mode-page {
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.check-mode-page__scroll {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.mode-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.mode-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 32px 24px;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 12px rgba(15, 23, 42, 0.06);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
box-shadow 0.15s,
|
||||
transform 0.15s;
|
||||
}
|
||||
|
||||
.mode-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.1);
|
||||
}
|
||||
|
||||
.mode-card:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.mode-card__icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mode-card__title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.mode-card__desc {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import CheckoutStepper from '@/components/layout/CheckoutStepper.vue'
|
||||
import IconCashSettlement from '@/components/icons/IconCashSettlement.vue'
|
||||
import IconCheckBook from '@/components/icons/IconCheckBook.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const cartStore = useCartStore()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
function selectType(type: 'cash' | 'check') {
|
||||
if (!cartStore.activeCart) {
|
||||
router.push('/cart')
|
||||
return
|
||||
}
|
||||
orderStore.startCheckout(cartStore.activeCart, type)
|
||||
router.push(type === 'cash' ? '/checkout/cash' : '/checkout/check-mode')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Mobile -->
|
||||
<div class="page settlement-mobile-page mobile-only">
|
||||
<PageHeader
|
||||
title="انتخاب نوع تسویه"
|
||||
subtitle="نوع تسویه را انتخاب کنید"
|
||||
show-back
|
||||
centered
|
||||
@back="router.push('/cart')"
|
||||
/>
|
||||
<div class="page__scroll">
|
||||
<div class="settlement-mobile-options">
|
||||
<button type="button" class="settlement-card" @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" @click="selectType('check')">
|
||||
<span class="settlement-card__icon settlement-card__icon--check">
|
||||
<IconCheckBook :size="36" />
|
||||
</span>
|
||||
<h3>تسویه چکی</h3>
|
||||
<p>پرداخت از طریق ثبت چک</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop -->
|
||||
<div class="desktop-checkout-shell desktop-only">
|
||||
<CheckoutStepper :current-step="2" />
|
||||
<header class="desktop-page__head">
|
||||
<h1 class="desktop-page__title">انتخاب نوع تسویه</h1>
|
||||
<p class="desktop-page__subtitle">نوع تسویه را انتخاب کنید</p>
|
||||
</header>
|
||||
<div class="desktop-settlement-grid">
|
||||
<button type="button" class="settlement-card settlement-card--desktop" @click="selectType('cash')">
|
||||
<span class="settlement-card__icon settlement-card__icon--cash">
|
||||
<IconCashSettlement :size="40" />
|
||||
</span>
|
||||
<h3>تسویه نقدی</h3>
|
||||
<p>پرداخت از طریق واریز به حساب شرکت</p>
|
||||
</button>
|
||||
<button type="button" class="settlement-card settlement-card--desktop" @click="selectType('check')">
|
||||
<span class="settlement-card__icon settlement-card__icon--check">
|
||||
<IconCheckBook :size="40" />
|
||||
</span>
|
||||
<h3>تسویه چکی</h3>
|
||||
<p>پرداخت از طریق ثبت چک</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settlement-mobile-page {
|
||||
background: var(--color-bg);
|
||||
padding: 12px 0 0;
|
||||
}
|
||||
|
||||
.settlement-mobile-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.settlement-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 32px 24px;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
box-shadow: var(--shadow-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settlement-card--desktop {
|
||||
min-height: 220px;
|
||||
border: 1px solid var(--color-border);
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.settlement-card--desktop:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.settlement-card__icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.settlement-card__icon--cash {
|
||||
background: var(--color-success-light);
|
||||
}
|
||||
|
||||
.settlement-card__icon--check {
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.settlement-card h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.0625rem;
|
||||
}
|
||||
|
||||
.settlement-card p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useCatalogStore } from '@/stores/catalog'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import IconParLogo from '@/components/icons/IconParLogo.vue'
|
||||
import IconFamBaftLogo from '@/components/icons/IconFamBaftLogo.vue'
|
||||
import MobileCompanySelect from '@/views/companies/components/MobileCompanySelect.vue'
|
||||
import DesktopHomeDashboard from '@/views/companies/components/DesktopHomeDashboard.vue'
|
||||
import type { Company } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
const catalogStore = useCatalogStore()
|
||||
const cartStore = useCartStore()
|
||||
|
||||
const companies = catalogStore.getActiveCompanies()
|
||||
const showSwitchDialog = ref(false)
|
||||
const pendingCompanyId = ref<string | null>(null)
|
||||
const switchMessage = ref('')
|
||||
|
||||
const displayMeta: Record<string, { description: string; logo: typeof IconParLogo }> = {
|
||||
par: { description: 'تولید و عرضه انواع پارچه', logo: IconParLogo },
|
||||
'fam-baft': { description: 'تولید و عرضه انواع پارچه', logo: IconFamBaftLogo },
|
||||
}
|
||||
|
||||
function selectCompany(companyId: string) {
|
||||
const check = cartStore.canSwitchToCompany(companyId)
|
||||
if (!check.allowed) {
|
||||
pendingCompanyId.value = companyId
|
||||
switchMessage.value = check.message ?? ''
|
||||
showSwitchDialog.value = true
|
||||
return
|
||||
}
|
||||
goToCompany(companyId)
|
||||
}
|
||||
|
||||
function goToCompany(companyId: string) {
|
||||
const company = catalogStore.getCompany(companyId)
|
||||
if (!company) return
|
||||
cartStore.initCartForCompany(company)
|
||||
router.push(`/companies/${companyId}/catalogs`)
|
||||
}
|
||||
|
||||
function confirmSwitch() {
|
||||
if (pendingCompanyId.value) {
|
||||
goToCompany(pendingCompanyId.value)
|
||||
}
|
||||
showSwitchDialog.value = false
|
||||
}
|
||||
|
||||
function cardVariant(slug: string) {
|
||||
return slug === 'par' ? 'par' : 'fam-baft'
|
||||
}
|
||||
|
||||
function companyDescription(company: Company) {
|
||||
return displayMeta[company.slug]?.description ?? company.description
|
||||
}
|
||||
|
||||
function companyLogo(slug: string) {
|
||||
return displayMeta[slug]?.logo ?? IconParLogo
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MobileCompanySelect
|
||||
class="mobile-only"
|
||||
:companies="companies"
|
||||
:card-variant="cardVariant"
|
||||
:company-description="companyDescription"
|
||||
:company-logo="companyLogo"
|
||||
@select="selectCompany"
|
||||
/>
|
||||
|
||||
<DesktopHomeDashboard
|
||||
class="desktop-only"
|
||||
:companies="companies"
|
||||
:card-variant="cardVariant"
|
||||
:company-description="companyDescription"
|
||||
:company-logo="companyLogo"
|
||||
@select="selectCompany"
|
||||
/>
|
||||
|
||||
<div v-if="showSwitchDialog" class="dialog-overlay" @click.self="showSwitchDialog = false">
|
||||
<div class="dialog card">
|
||||
<h3>تغییر شرکت</h3>
|
||||
<p class="text-sm">{{ switchMessage }}</p>
|
||||
<div class="dialog-actions">
|
||||
<BaseButton variant="outline" @click="showSwitchDialog = false">انصراف</BaseButton>
|
||||
<BaseButton @click="confirmSwitch">سبد جدید</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dialog h3 {
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.dialog-actions {
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import type { Company } from '@/types'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
companies: Company[]
|
||||
cardVariant: (slug: string) => string
|
||||
companyDescription: (company: Company) => string
|
||||
companyLogo: (slug: string) => Component
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
select: [companyId: string]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-page company-desktop-page">
|
||||
<header class="desktop-page__head">
|
||||
<h1 class="desktop-page__title">انتخاب شرکت</h1>
|
||||
<p class="desktop-page__subtitle">شرکت مورد نظر خود را انتخاب کنید</p>
|
||||
</header>
|
||||
|
||||
<div class="desktop-company-grid">
|
||||
<button
|
||||
v-for="company in companies"
|
||||
:key="company.id"
|
||||
type="button"
|
||||
class="company-brand-card"
|
||||
:class="`company-brand-card--${cardVariant(company.slug)}`"
|
||||
@click="$emit('select', company.id)"
|
||||
>
|
||||
<span class="company-brand-card__logo">
|
||||
<component :is="companyLogo(company.slug)" :size="80" />
|
||||
</span>
|
||||
<h2 class="company-brand-card__name">{{ company.name }}</h2>
|
||||
<p class="company-brand-card__desc">{{ companyDescription(company) }}</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.company-desktop-page {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.company-brand-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 240px;
|
||||
padding: 36px 28px;
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.company-brand-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.company-brand-card--par {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.company-brand-card--fam-baft {
|
||||
background: #f3ece3;
|
||||
color: #1a202c;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.company-brand-card__logo {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.company-brand-card__name {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.company-brand-card__desc {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
max-width: 280px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.company-brand-card--par .company-brand-card__desc {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.company-brand-card--fam-baft .company-brand-card__desc {
|
||||
color: #718096;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,573 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconHome from '@/components/icons/IconHome.vue'
|
||||
import IconCatalog from '@/components/icons/IconCatalog.vue'
|
||||
import IconCart from '@/components/icons/IconCart.vue'
|
||||
import IconOrders from '@/components/icons/IconOrders.vue'
|
||||
import IconShoppingBag from '@/components/icons/IconShoppingBag.vue'
|
||||
import IconBuilding from '@/components/icons/IconBuilding.vue'
|
||||
import IconLayoutGrid from '@/components/icons/IconLayoutGrid.vue'
|
||||
import type { Company } from '@/types'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
companies: Company[]
|
||||
cardVariant: (slug: string) => string
|
||||
companyDescription: (company: Company) => string
|
||||
companyLogo: (slug: string) => Component
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ select: [companyId: string] }>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const displayCompanies = computed(() =>
|
||||
[...props.companies].sort((a, b) => {
|
||||
if (a.slug === 'fam-baft') return -1
|
||||
if (b.slug === 'fam-baft') return 1
|
||||
return 0
|
||||
}),
|
||||
)
|
||||
|
||||
const quickLinks = [
|
||||
{
|
||||
label: 'انتخاب شرکت',
|
||||
icon: IconHome,
|
||||
path: '/',
|
||||
tone: 'orange',
|
||||
},
|
||||
{
|
||||
label: 'سفارشگذاری',
|
||||
icon: IconCatalog,
|
||||
path: '/',
|
||||
tone: 'blue',
|
||||
},
|
||||
{
|
||||
label: 'سبد خرید',
|
||||
icon: IconCart,
|
||||
path: '/cart',
|
||||
tone: 'green',
|
||||
},
|
||||
{
|
||||
label: 'سفارشهای من',
|
||||
icon: IconOrders,
|
||||
path: '/orders',
|
||||
tone: 'purple',
|
||||
},
|
||||
]
|
||||
|
||||
const orderSteps = [
|
||||
{ id: 1, label: 'انتخاب شرکت', icon: IconBuilding, active: true },
|
||||
{ id: 2, label: 'انتخاب کالیته', icon: IconLayoutGrid, active: false },
|
||||
{ id: 3, label: 'ثبت سفارش', icon: IconOrders, active: false },
|
||||
]
|
||||
|
||||
const currentStep = 1
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-page home-dashboard">
|
||||
<header class="home-dashboard__head">
|
||||
<h1 class="home-dashboard__title">انتخاب شرکت</h1>
|
||||
<p class="home-dashboard__subtitle">شرکت موردنظر خود را برای شروع سفارشگذاری انتخاب کنید</p>
|
||||
</header>
|
||||
|
||||
<section class="welcome-banner" aria-label="خوشآمدگویی">
|
||||
<div class="welcome-banner__visual" aria-hidden="true">
|
||||
<div class="welcome-banner__fabric">
|
||||
<span class="welcome-banner__roll welcome-banner__roll--1" />
|
||||
<span class="welcome-banner__roll welcome-banner__roll--2" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="welcome-banner__content">
|
||||
<h2 class="welcome-banner__heading">به پارس بافت خوش آمدید</h2>
|
||||
<p class="welcome-banner__text">
|
||||
برای شروع سفارش، ابتدا شرکت موردنظر را انتخاب کنید و سپس کالیته و محصولات را اضافه
|
||||
کنید.
|
||||
</p>
|
||||
</div>
|
||||
<div class="welcome-banner__icon" aria-hidden="true">
|
||||
<IconShoppingBag :size="26" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="home-dashboard__companies">
|
||||
<article
|
||||
v-for="company in displayCompanies"
|
||||
:key="company.id"
|
||||
class="company-showcase"
|
||||
:class="`company-showcase--${cardVariant(company.slug)}`"
|
||||
>
|
||||
<div class="company-showcase__pattern" aria-hidden="true" />
|
||||
<div class="company-showcase__body">
|
||||
<span class="company-showcase__logo">
|
||||
<component :is="companyLogo(company.slug)" :size="72" />
|
||||
</span>
|
||||
<h3 class="company-showcase__name">{{ company.name }}</h3>
|
||||
<p class="company-showcase__desc">{{ companyDescription(company) }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="company-showcase__cta"
|
||||
@click="emit('select', company.id)"
|
||||
>
|
||||
<span class="company-showcase__cta-text">ورود به کاتالوگها</span>
|
||||
<IconChevronLeft :size="16" class="company-showcase__cta-icon" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="home-dashboard__bottom">
|
||||
<section class="home-panel home-panel--quick">
|
||||
<h3 class="home-panel__title">دسترسی سریع</h3>
|
||||
<div class="quick-access">
|
||||
<button
|
||||
v-for="link in quickLinks"
|
||||
:key="link.label"
|
||||
type="button"
|
||||
class="quick-access__item"
|
||||
@click="router.push(link.path)"
|
||||
>
|
||||
<span class="quick-access__icon" :class="`quick-access__icon--${link.tone}`">
|
||||
<component :is="link.icon" :size="22" />
|
||||
</span>
|
||||
<span class="quick-access__label">{{ link.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="home-panel home-panel--steps">
|
||||
<h3 class="home-panel__title">مراحل ثبت سفارش</h3>
|
||||
<ol class="order-steps">
|
||||
<template v-for="(step, index) in orderSteps" :key="step.id">
|
||||
<li
|
||||
class="order-step"
|
||||
:class="{ 'order-step--active': step.id === currentStep }"
|
||||
>
|
||||
<span class="order-step__dot">{{ step.id }}</span>
|
||||
<span class="order-step__icon">
|
||||
<component :is="step.icon" :size="18" />
|
||||
</span>
|
||||
<span class="order-step__label">{{ step.label }}</span>
|
||||
</li>
|
||||
<li
|
||||
v-if="index < orderSteps.length - 1"
|
||||
class="order-step-bridge"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span
|
||||
class="order-step-bridge__line"
|
||||
:class="{ 'order-step-bridge__line--done': index < currentStep }"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-dashboard {
|
||||
max-width: 980px;
|
||||
margin-inline: auto;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.home-dashboard__head {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.home-dashboard__title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.home-dashboard__subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.welcome-banner {
|
||||
direction: ltr;
|
||||
display: grid;
|
||||
grid-template-columns: 132px 1fr 56px;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 18px 22px;
|
||||
margin-bottom: 24px;
|
||||
background: linear-gradient(90deg, #fff 0%, #fff 58%, var(--color-primary-soft) 58%);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.welcome-banner__visual {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.welcome-banner__fabric {
|
||||
position: relative;
|
||||
width: 112px;
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
.welcome-banner__roll {
|
||||
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);
|
||||
}
|
||||
|
||||
.welcome-banner__roll--1 {
|
||||
width: 36px;
|
||||
height: 52px;
|
||||
left: 8px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.welcome-banner__roll--2 {
|
||||
width: 30px;
|
||||
height: 44px;
|
||||
left: 44px;
|
||||
background: linear-gradient(180deg, #e2e8f0 0%, #94a3b8 55%, #64748b 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.welcome-banner__content {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.welcome-banner__heading {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.welcome-banner__text {
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.65;
|
||||
max-width: 460px;
|
||||
}
|
||||
|
||||
.welcome-banner__icon {
|
||||
justify-self: end;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.home-dashboard__companies {
|
||||
direction: ltr;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.company-showcase {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
min-height: 292px;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.company-showcase__pattern {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.company-showcase--par {
|
||||
background: #17379f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.company-showcase--par .company-showcase__pattern {
|
||||
opacity: 0.22;
|
||||
background:
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.18) 0%, transparent 42%),
|
||||
radial-gradient(circle at 80% 70%, rgba(255, 255, 255, 0.12) 0%, transparent 38%),
|
||||
repeating-linear-gradient(
|
||||
115deg,
|
||||
transparent,
|
||||
transparent 14px,
|
||||
rgba(255, 255, 255, 0.07) 14px,
|
||||
rgba(255, 255, 255, 0.07) 28px
|
||||
);
|
||||
}
|
||||
|
||||
.company-showcase--fam-baft {
|
||||
background: #f3ece3;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.company-showcase--fam-baft .company-showcase__pattern {
|
||||
opacity: 0.35;
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
transparent 8px,
|
||||
rgba(0, 0, 0, 0.025) 8px,
|
||||
rgba(0, 0, 0, 0.025) 16px
|
||||
);
|
||||
}
|
||||
|
||||
.company-showcase__body {
|
||||
direction: rtl;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 34px 24px 28px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.company-showcase__logo {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.company-showcase__name {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.company-showcase__desc {
|
||||
margin: 0 0 22px;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.65;
|
||||
max-width: 260px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.company-showcase--par .company-showcase__desc {
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.company-showcase--fam-baft .company-showcase__desc {
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.company-showcase__cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 11px 22px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.company-showcase--par .company-showcase__cta {
|
||||
background: #fff;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.company-showcase--fam-baft .company-showcase__cta {
|
||||
background: #c9b896;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.company-showcase__cta:hover {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.company-showcase__cta-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.home-dashboard__bottom {
|
||||
direction: ltr;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.home-panel {
|
||||
direction: rtl;
|
||||
padding: 20px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.home-panel__title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.quick-access {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.quick-access__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 4px 6px 0;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.quick-access__icon {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
.quick-access__item:hover .quick-access__icon {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.quick-access__icon--orange {
|
||||
background: #fff4e5;
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.quick-access__icon--blue {
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.quick-access__icon--green {
|
||||
background: var(--color-success-soft);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.quick-access__icon--purple {
|
||||
background: var(--color-purple-light);
|
||||
color: var(--color-purple);
|
||||
}
|
||||
|
||||
.quick-access__label {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.order-steps {
|
||||
direction: rtl;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.order-step {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.order-step-bridge {
|
||||
flex: 1 1 0;
|
||||
min-width: 16px;
|
||||
list-style: none;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.order-step-bridge__line {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border-top: 2px dashed var(--color-border);
|
||||
}
|
||||
|
||||
.order-step-bridge__line--done {
|
||||
border-top-color: #93a8e8;
|
||||
}
|
||||
|
||||
.order-step__dot {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.order-step--active .order-step__dot {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.order-step__icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: var(--color-text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.order-step--active .order-step__icon {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.order-step__label {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.order-step--active .order-step__label {
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,126 @@
|
||||
<script setup lang="ts">
|
||||
import type { Company } from '@/types'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
companies: Company[]
|
||||
cardVariant: (slug: string) => string
|
||||
companyDescription: (company: Company) => string
|
||||
companyLogo: (slug: string) => Component
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
select: [companyId: string]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="company-mobile-page">
|
||||
<header class="company-mobile-page__header">
|
||||
<h1 class="company-mobile-page__title">انتخاب شرکت</h1>
|
||||
<p class="company-mobile-page__subtitle">شرکت مورد نظر خود را انتخاب کنید</p>
|
||||
</header>
|
||||
|
||||
<div class="company-mobile-page__list">
|
||||
<button
|
||||
v-for="company in companies"
|
||||
:key="company.id"
|
||||
type="button"
|
||||
class="company-brand-card"
|
||||
:class="`company-brand-card--${cardVariant(company.slug)}`"
|
||||
@click="$emit('select', company.id)"
|
||||
>
|
||||
<span class="company-brand-card__logo">
|
||||
<component :is="companyLogo(company.slug)" :size="72" />
|
||||
</span>
|
||||
<h2 class="company-brand-card__name">{{ company.name }}</h2>
|
||||
<p class="company-brand-card__desc">{{ companyDescription(company) }}</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.company-mobile-page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: #fff;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.company-mobile-page__header {
|
||||
text-align: center;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.company-mobile-page__title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.company-mobile-page__subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.company-mobile-page__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.company-brand-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
padding: 32px 24px;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.company-brand-card--par {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 24px rgba(29, 53, 87, 0.25);
|
||||
}
|
||||
|
||||
.company-brand-card--fam-baft {
|
||||
background: #f3ece3;
|
||||
color: #1a202c;
|
||||
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.company-brand-card__logo {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.company-brand-card__name {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.company-brand-card__desc {
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.company-brand-card--par .company-brand-card__desc {
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
.company-brand-card--fam-baft .company-brand-card__desc {
|
||||
color: #718096;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,686 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import BaseInput from '@/components/ui/BaseInput.vue'
|
||||
import CurrencyInput from '@/components/ui/CurrencyInput.vue'
|
||||
import PersianDateInput from '@/components/ui/PersianDateInput.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import {
|
||||
formatMeterage,
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
formatDateTime,
|
||||
formatNumber,
|
||||
parseFormattedNumber,
|
||||
} from '@/utils/format'
|
||||
import { validateImageFile, fileToBase64 } from '@/utils/validation'
|
||||
import { daysUntilDue } from '@/utils/checkRass'
|
||||
import {
|
||||
FORM_VALIDATION_SUMMARY,
|
||||
clearFieldError,
|
||||
hasFieldErrors,
|
||||
type FieldErrors,
|
||||
} from '@/utils/formErrors'
|
||||
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'
|
||||
|
||||
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 showTracking = ref(route.query.track === '1' || route.query.receipt === '1')
|
||||
const showReceiptForm = ref(route.query.receipt === '1')
|
||||
const showOrderDetails = ref(route.query.receipt === '1')
|
||||
const receiptAmount = ref('')
|
||||
const receiptDate = ref('')
|
||||
const receiptTime = ref('')
|
||||
const trackingNumber = ref('')
|
||||
const destinationAccountId = ref('')
|
||||
const receiptImage = ref('')
|
||||
const fieldErrors = ref<FieldErrors>({})
|
||||
const formError = ref('')
|
||||
|
||||
watch(receiptAmount, () => {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, 'receiptAmount')
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
})
|
||||
watch(receiptDate, () => {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, 'receiptDate')
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
})
|
||||
watch(trackingNumber, () => {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, 'trackingNumber')
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
})
|
||||
watch(destinationAccountId, () => {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, 'destinationAccountId')
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
})
|
||||
|
||||
const bankAccounts = computed(() =>
|
||||
order.value ? orderStore.getBankAccountsForCompany(order.value.companyId) : [],
|
||||
)
|
||||
|
||||
const statusHero = computed(() => (order.value ? getStatusHero(order.value.status) : null))
|
||||
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') {
|
||||
return type === 'cash' ? 'تسویه نقدی' : 'تسویه چکی'
|
||||
}
|
||||
|
||||
function openTracking() {
|
||||
showTracking.value = true
|
||||
showOrderDetails.value = false
|
||||
router.replace({ query: { track: '1' } })
|
||||
}
|
||||
|
||||
function openOrderDetails() {
|
||||
showOrderDetails.value = true
|
||||
}
|
||||
|
||||
async function handleReceiptImage(event: Event) {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
const v = validateImageFile(file)
|
||||
if (!v.valid) {
|
||||
fieldErrors.value = { ...fieldErrors.value, receiptImage: v.message ?? 'خطا' }
|
||||
return
|
||||
}
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, 'receiptImage')
|
||||
receiptImage.value = await fileToBase64(file)
|
||||
}
|
||||
|
||||
function submitReceipt() {
|
||||
if (!order.value) return
|
||||
|
||||
fieldErrors.value = {}
|
||||
formError.value = ''
|
||||
|
||||
const amount = parseFormattedNumber(receiptAmount.value)
|
||||
const errors: FieldErrors = {}
|
||||
|
||||
if (!amount) {
|
||||
errors.receiptAmount = 'مبلغ واریزی الزامی است.'
|
||||
}
|
||||
if (!receiptDate.value) {
|
||||
errors.receiptDate = 'تاریخ واریز الزامی است.'
|
||||
}
|
||||
if (!trackingNumber.value.trim()) {
|
||||
errors.trackingNumber = 'شماره پیگیری الزامی است.'
|
||||
}
|
||||
if (!destinationAccountId.value) {
|
||||
errors.destinationAccountId = 'حساب مقصد را انتخاب کنید.'
|
||||
}
|
||||
|
||||
if (hasFieldErrors(errors)) {
|
||||
fieldErrors.value = errors
|
||||
formError.value = FORM_VALIDATION_SUMMARY
|
||||
return
|
||||
}
|
||||
|
||||
orderStore.submitCashReceipt(order.value.id, {
|
||||
amount,
|
||||
transferDate: receiptDate.value,
|
||||
transferTime: receiptTime.value || '12:00',
|
||||
trackingNumber: trackingNumber.value,
|
||||
destinationAccountId: destinationAccountId.value,
|
||||
receiptImage: receiptImage.value || undefined,
|
||||
})
|
||||
showReceiptForm.value = false
|
||||
fieldErrors.value = {}
|
||||
formError.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="order" class="order-detail-page">
|
||||
<header class="order-detail-page__nav mobile-only">
|
||||
<button type="button" class="nav-btn" aria-label="بازگشت" @click="router.push('/orders')">
|
||||
<IconChevronBack :size="20" />
|
||||
</button>
|
||||
<h1 class="order-detail-page__title">{{ showTracking ? 'پیگیری سفارش' : 'جزئیات سفارش' }}</h1>
|
||||
<button type="button" class="nav-btn nav-btn--ghost" aria-hidden="true" tabindex="-1">
|
||||
<IconChevronBack :size="18" class="nav-btn__flip" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<header class="desktop-page__head desktop-only order-detail-page__desktop-head">
|
||||
<button type="button" class="order-detail-back" @click="router.push('/orders')">
|
||||
← بازگشت به سفارشها
|
||||
</button>
|
||||
<h1 class="desktop-page__title">
|
||||
{{ showTracking ? 'پیگیری سفارش' : 'جزئیات سفارش' }}
|
||||
</h1>
|
||||
<p class="desktop-page__subtitle">{{ order.orderNumber }} · {{ order.companyName }}</p>
|
||||
</header>
|
||||
|
||||
<div class="order-detail-page__body">
|
||||
<p v-if="showTracking" class="order-detail-page__order-id">{{ order.orderNumber }}</p>
|
||||
|
||||
<div v-if="!showTracking" class="order-info-card">
|
||||
<div class="order-info-card__head">
|
||||
<span class="order-info-card__label">شماره سفارش</span>
|
||||
<strong class="order-info-card__number">{{ order.orderNumber }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="order-info-card__row">
|
||||
<span class="order-info-card__key">شرکت</span>
|
||||
<span class="order-info-card__value order-info-card__value--accent">{{ order.companyName }}</span>
|
||||
</div>
|
||||
|
||||
<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) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="order-info-card__row">
|
||||
<span class="order-info-card__key">نوع تسویه</span>
|
||||
<span class="order-info-card__value order-info-card__value--accent">
|
||||
{{ settlementLabel(order.settlementType) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="order-info-card__row order-info-card__row--last">
|
||||
<span class="order-info-card__key">مبلغ فاکتور</span>
|
||||
<strong class="order-info-card__amount">
|
||||
{{ order.finalAmount ? formatNumber(order.finalAmount) : '—' }}
|
||||
</strong>
|
||||
</div>
|
||||
</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">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<template v-if="order.settlementType === 'cash'">
|
||||
<div v-if="order.cashInvoiceExpiresAt" class="tracking-alert">
|
||||
اعتبار فاکتور: {{ formatDateTime(order.cashInvoiceExpiresAt) }}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<BaseButton
|
||||
v-if="!order.cashReceipt && order.status !== 'expired'"
|
||||
block
|
||||
@click="showReceiptForm = true"
|
||||
>
|
||||
ثبت رسید پرداخت
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showTracking && !showOrderDetails" class="order-detail-page__footer mobile-only">
|
||||
<BaseButton block @click="openOrderDetails">{{ trackingFooterLabel }}</BaseButton>
|
||||
</div>
|
||||
|
||||
<div v-if="!showTracking" class="order-detail-page__footer mobile-only">
|
||||
<BaseButton block @click="openTracking">مشاهده جزئیات و پیگیری</BaseButton>
|
||||
</div>
|
||||
|
||||
<div 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">
|
||||
مشاهده جزئیات و پیگیری
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div v-if="showReceiptForm" class="dialog-overlay" @click.self="showReceiptForm = false">
|
||||
<div class="dialog card">
|
||||
<h3>ثبت رسید پرداخت</h3>
|
||||
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<CurrencyInput
|
||||
v-model="receiptAmount"
|
||||
label="مبلغ واریزی"
|
||||
:error="fieldErrors.receiptAmount"
|
||||
/>
|
||||
<PersianDateInput
|
||||
v-model="receiptDate"
|
||||
label="تاریخ واریز"
|
||||
:error="fieldErrors.receiptDate"
|
||||
/>
|
||||
<BaseInput v-model="receiptTime" label="ساعت واریز" type="time" />
|
||||
<BaseInput
|
||||
v-model="trackingNumber"
|
||||
label="شماره پیگیری"
|
||||
:error="fieldErrors.trackingNumber"
|
||||
/>
|
||||
<div class="form-group">
|
||||
<label class="form-label">حساب مقصد</label>
|
||||
<select
|
||||
v-model="destinationAccountId"
|
||||
class="select-input"
|
||||
:class="{ 'select-input--error': fieldErrors.destinationAccountId }"
|
||||
>
|
||||
<option value="">انتخاب کنید</option>
|
||||
<option v-for="acc in bankAccounts" :key="acc.id" :value="acc.id">
|
||||
{{ acc.bankName }} - {{ acc.cardNumber }}
|
||||
</option>
|
||||
</select>
|
||||
<p v-if="fieldErrors.destinationAccountId" class="input-error-text">
|
||||
{{ fieldErrors.destinationAccountId }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">تصویر رسید</label>
|
||||
<input type="file" accept="image/*" @change="handleReceiptImage" />
|
||||
<p v-if="fieldErrors.receiptImage" class="input-error-text">{{ fieldErrors.receiptImage }}</p>
|
||||
</div>
|
||||
<BaseButton block class="mt-4" @click="submitReceipt">ثبت رسید</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="page empty-state">
|
||||
<p>سفارش یافت نشد</p>
|
||||
<BaseButton @click="router.push('/orders')">بازگشت</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.order-detail-page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
padding: 12px var(--page-padding) 0;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.order-detail-page {
|
||||
padding: 20px var(--page-padding-tablet) 28px;
|
||||
background: transparent;
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.order-detail-page__nav {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 40px;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.order-detail-page__title {
|
||||
margin: 0;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-btn--ghost {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-btn__flip {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.order-detail-page__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.order-detail-page__order-id {
|
||||
margin: -8px 0 16px;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.order-info-card {
|
||||
background: #ffffff;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 4px 20px rgba(15, 23, 42, 0.08);
|
||||
padding: 20px 18px 8px;
|
||||
}
|
||||
|
||||
.order-info-card__head {
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #eeeeee;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.order-info-card__label {
|
||||
display: block;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.order-info-card__number {
|
||||
display: block;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.order-info-card__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid #eeeeee;
|
||||
}
|
||||
|
||||
.order-info-card__row--last {
|
||||
border-bottom: none;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.order-info-card__key {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.order-info-card__value {
|
||||
font-size: 0.875rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.order-info-card__value--accent {
|
||||
color: #2b66e3;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.order-info-card__amount {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.order-detail-page__footer {
|
||||
flex-shrink: 0;
|
||||
padding: 12px 0;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid var(--color-border);
|
||||
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.06);
|
||||
margin: 0 calc(-1 * var(--page-padding));
|
||||
padding-left: var(--page-padding);
|
||||
padding-right: var(--page-padding);
|
||||
}
|
||||
|
||||
.order-detail-page__footer :deep(.btn) {
|
||||
border-radius: 12px;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.tracking-card {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 16px;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.tracking-card__title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.order-detail-page__tracking :deep(.tracking-summary) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.tracking-alert {
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: var(--color-info-light);
|
||||
color: var(--color-primary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.item-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.item-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-row__meta {
|
||||
margin: 4px 0 0;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.item-row--summary {
|
||||
margin-top: 4px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.875rem;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.bank-row {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.bank-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.bank-row__meta {
|
||||
margin: 4px 0 0;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.rass-display {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-purple);
|
||||
}
|
||||
|
||||
.rass-date {
|
||||
margin: 6px 0 0;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.check-img {
|
||||
max-width: 100%;
|
||||
max-height: 100px;
|
||||
margin-top: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
width: 100%;
|
||||
max-width: min(480px, calc(100vw - 32px));
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.select-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1.5px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.select-input--error {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.input-error-text {
|
||||
color: var(--color-danger);
|
||||
font-size: 0.8125rem;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
min-height: 240px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import { formatMeterage } from '@/utils/format'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const order = computed(() => orderStore.getOrderById(route.params.id as string))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page success-page">
|
||||
<div class="page__scroll success-page__scroll">
|
||||
<div class="success-icon">✓</div>
|
||||
<h1 class="page-title text-center">سفارش ثبت شد</h1>
|
||||
<p class="text-center text-muted mb-4">
|
||||
سفارش شما با موفقیت ثبت شد. اطلاعات محصولات و شرایط تسویه توسط واحد فروش بررسی شده و نتیجه از
|
||||
طریق پیامک و پنل کاربری اعلام میشود.
|
||||
</p>
|
||||
|
||||
<div v-if="order" class="card mb-4">
|
||||
<div class="info-row">
|
||||
<span>شماره سفارش:</span>
|
||||
<strong>{{ order.orderNumber }}</strong>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span>شرکت:</span>
|
||||
<span>{{ order.companyName }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span>متراژ:</span>
|
||||
<span>{{ formatMeterage(order.totalMeterage) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span>نوع تسویه:</span>
|
||||
<span>{{ order.settlementType === 'cash' ? 'نقدی' : 'چکی' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseButton block @click="router.replace(`/orders/${order?.id}`)">
|
||||
مشاهده جزئیات سفارش
|
||||
</BaseButton>
|
||||
<BaseButton block variant="outline" class="mt-2" @click="router.replace('/')">
|
||||
بازگشت به خانه
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.success-page {
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success-page__scroll {
|
||||
padding-top: 48px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success);
|
||||
font-size: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 24px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,210 @@
|
||||
<script setup lang="ts">
|
||||
import EmptyState from '@/components/ui/EmptyState.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||||
import IconOrders from '@/components/icons/IconOrders.vue'
|
||||
import IconHourglass from '@/components/icons/IconHourglass.vue'
|
||||
import IconProgressClock from '@/components/icons/IconProgressClock.vue'
|
||||
import IconCheckCircleOutline from '@/components/icons/IconCheckCircleOutline.vue'
|
||||
import IconFilter from '@/components/icons/IconFilter.vue'
|
||||
import IconLayoutGrid from '@/components/icons/IconLayoutGrid.vue'
|
||||
import IconMoreVertical from '@/components/icons/IconMoreVertical.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import { formatDateTime, formatNumber } from '@/utils/format'
|
||||
import type { Order } from '@/types'
|
||||
import type { DesktopOrderFilterId } from '@/utils/orderStatusGroups'
|
||||
|
||||
defineProps<{
|
||||
stats: { total: number; review: number; inProgress: number; done: number }
|
||||
desktopFilter: DesktopOrderFilterId
|
||||
desktopFilterTabs: { id: DesktopOrderFilterId; label: string }[]
|
||||
filteredOrders: Order[]
|
||||
paginatedOrders: Order[]
|
||||
paginationRange: { start: number; end: number }
|
||||
visiblePages: (number | 'ellipsis')[]
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
viewStatFilter: [filter: DesktopOrderFilterId]
|
||||
setDesktopFilter: [filter: DesktopOrderFilterId]
|
||||
select: [id: string]
|
||||
setPage: [page: number]
|
||||
newOrder: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-page orders-desktop-page">
|
||||
<header class="desktop-page__head">
|
||||
<h1 class="desktop-page__title">سفارشها</h1>
|
||||
<p class="desktop-page__subtitle">مدیریت و پیگیری سفارشهای ثبت شده</p>
|
||||
</header>
|
||||
|
||||
<div class="orders-stats desktop-grid-4">
|
||||
<article class="orders-stat-card">
|
||||
<div class="orders-stat-card__head">
|
||||
<span class="orders-stat-card__label orders-stat-card__label--total">همه سفارشها</span>
|
||||
<span class="orders-stat-card__icon orders-stat-card__icon--total" aria-hidden="true">
|
||||
<IconOrders :size="20" />
|
||||
</span>
|
||||
</div>
|
||||
<strong class="orders-stat-card__value">{{ formatNumber(stats.total) }}</strong>
|
||||
<button type="button" class="orders-stat-card__link" @click="emit('viewStatFilter', 'all')">
|
||||
مشاهده همه
|
||||
</button>
|
||||
</article>
|
||||
|
||||
<article class="orders-stat-card">
|
||||
<div class="orders-stat-card__head">
|
||||
<span class="orders-stat-card__label orders-stat-card__label--review">در انتظار بررسی</span>
|
||||
<span class="orders-stat-card__icon orders-stat-card__icon--review" aria-hidden="true">
|
||||
<IconHourglass :size="20" />
|
||||
</span>
|
||||
</div>
|
||||
<strong class="orders-stat-card__value">{{ formatNumber(stats.review) }}</strong>
|
||||
<button type="button" class="orders-stat-card__link" @click="emit('viewStatFilter', 'review')">
|
||||
مشاهده همه
|
||||
</button>
|
||||
</article>
|
||||
|
||||
<article class="orders-stat-card">
|
||||
<div class="orders-stat-card__head">
|
||||
<span class="orders-stat-card__label orders-stat-card__label--active">در حال انجام</span>
|
||||
<span class="orders-stat-card__icon orders-stat-card__icon--active" aria-hidden="true">
|
||||
<IconProgressClock :size="20" />
|
||||
</span>
|
||||
</div>
|
||||
<strong class="orders-stat-card__value">{{ formatNumber(stats.inProgress) }}</strong>
|
||||
<button
|
||||
type="button"
|
||||
class="orders-stat-card__link"
|
||||
@click="emit('viewStatFilter', 'in_progress')"
|
||||
>
|
||||
مشاهده همه
|
||||
</button>
|
||||
</article>
|
||||
|
||||
<article class="orders-stat-card">
|
||||
<div class="orders-stat-card__head">
|
||||
<span class="orders-stat-card__label orders-stat-card__label--done">تکمیل شده</span>
|
||||
<span class="orders-stat-card__icon orders-stat-card__icon--done" aria-hidden="true">
|
||||
<IconCheckCircleOutline :size="20" />
|
||||
</span>
|
||||
</div>
|
||||
<strong class="orders-stat-card__value">{{ formatNumber(stats.done) }}</strong>
|
||||
<button type="button" class="orders-stat-card__link" @click="emit('viewStatFilter', 'delivered')">
|
||||
مشاهده همه
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="desktop-toolbar">
|
||||
<div class="desktop-toolbar__actions">
|
||||
<button type="button" class="orders-toolbar__filter-btn">
|
||||
<IconFilter :size="18" />
|
||||
فیلترها
|
||||
</button>
|
||||
<button type="button" class="orders-toolbar__layout-btn" aria-label="نمای جدول">
|
||||
<IconLayoutGrid :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="orders-toolbar__pills" role="tablist" aria-label="فیلتر وضعیت">
|
||||
<button
|
||||
v-for="pill in desktopFilterTabs"
|
||||
:key="pill.id"
|
||||
type="button"
|
||||
role="tab"
|
||||
class="orders-filter-pill"
|
||||
:class="{ 'orders-filter-pill--active': desktopFilter === pill.id }"
|
||||
:aria-selected="desktopFilter === pill.id"
|
||||
@click="emit('setDesktopFilter', pill.id)"
|
||||
>
|
||||
{{ pill.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredOrders.length > 0" class="desktop-table-wrap">
|
||||
<div class="desktop-table-scroll">
|
||||
<table class="desktop-table orders-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">شماره سفارش</th>
|
||||
<th scope="col">شرکت</th>
|
||||
<th scope="col">مبلغ فاکتور (تومان)</th>
|
||||
<th scope="col">وضعیت</th>
|
||||
<th scope="col">تاریخ ثبت</th>
|
||||
<th scope="col">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="order in paginatedOrders" :key="order.id">
|
||||
<td class="orders-table__number">{{ order.orderNumber }}</td>
|
||||
<td>{{ order.companyName }}</td>
|
||||
<td class="orders-table__amount">
|
||||
{{ order.finalAmount ? formatNumber(order.finalAmount) : '—' }}
|
||||
</td>
|
||||
<td><StatusBadge :status="order.status" /></td>
|
||||
<td class="orders-table__date">{{ formatDateTime(order.createdAt) }}</td>
|
||||
<td>
|
||||
<div class="orders-table__actions">
|
||||
<button type="button" class="orders-table__action" @click="emit('select', order.id)">
|
||||
مشاهده
|
||||
</button>
|
||||
<button type="button" class="orders-table__menu" aria-label="گزینههای بیشتر">
|
||||
<IconMoreVertical :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="orders-pagination">
|
||||
<span class="orders-pagination__summary">
|
||||
نمایش {{ formatNumber(paginationRange.start) }} تا {{ formatNumber(paginationRange.end) }} از
|
||||
{{ formatNumber(filteredOrders.length) }} سفارش
|
||||
</span>
|
||||
<div class="orders-pagination__controls">
|
||||
<button
|
||||
type="button"
|
||||
class="orders-pagination__arrow"
|
||||
:disabled="currentPage <= 1"
|
||||
aria-label="صفحه قبل"
|
||||
@click="emit('setPage', currentPage - 1)"
|
||||
>
|
||||
<IconChevronLeft :size="16" class="orders-pagination__arrow-icon--next" />
|
||||
</button>
|
||||
<template v-for="(page, index) in visiblePages" :key="`${page}-${index}`">
|
||||
<span v-if="page === 'ellipsis'" class="orders-pagination__ellipsis">…</span>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="orders-pagination__page"
|
||||
:class="{ 'orders-pagination__page--active': page === currentPage }"
|
||||
@click="emit('setPage', page)"
|
||||
>
|
||||
{{ formatNumber(page) }}
|
||||
</button>
|
||||
</template>
|
||||
<button
|
||||
type="button"
|
||||
class="orders-pagination__arrow"
|
||||
:disabled="currentPage >= totalPages"
|
||||
aria-label="صفحه بعد"
|
||||
@click="emit('setPage', currentPage + 1)"
|
||||
>
|
||||
<IconChevronLeft :size="16" class="orders-pagination__arrow-icon--prev" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EmptyState v-else message="سفارشی یافت نشد">
|
||||
<BaseButton @click="emit('newOrder')">ثبت سفارش جدید</BaseButton>
|
||||
</EmptyState>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,199 @@
|
||||
<script setup lang="ts">
|
||||
import EmptyState from '@/components/ui/EmptyState.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import FilterTabs from '@/components/ui/FilterTabs.vue'
|
||||
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import IconParLogo from '@/components/icons/IconParLogo.vue'
|
||||
import IconFamBaftLogo from '@/components/icons/IconFamBaftLogo.vue'
|
||||
import { formatCurrency } from '@/utils/format'
|
||||
import type { Order } from '@/types'
|
||||
|
||||
defineProps<{
|
||||
tab: string
|
||||
tabs: { id: string; label: string }[]
|
||||
orders: Order[]
|
||||
companySlug: (order: Order) => string
|
||||
iconVariant: (order: Order) => string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:tab': [value: string]
|
||||
select: [id: string]
|
||||
back: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page orders-mobile-page">
|
||||
<header class="orders-mobile-page__nav">
|
||||
<button type="button" class="nav-btn" aria-label="بازگشت" @click="emit('back')">
|
||||
<IconChevronBack :size="20" />
|
||||
</button>
|
||||
<h1 class="orders-mobile-page__title">سفارشها</h1>
|
||||
<button type="button" class="nav-btn nav-btn--ghost" aria-hidden="true" tabindex="-1">
|
||||
<IconChevronBack :size="18" class="nav-btn__flip" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<FilterTabs
|
||||
:model-value="tab"
|
||||
:tabs="tabs"
|
||||
equal
|
||||
class="orders-mobile-page__tabs"
|
||||
@update:model-value="emit('update:tab', $event)"
|
||||
/>
|
||||
|
||||
<div class="page__scroll page__scroll--with-nav">
|
||||
<div v-if="orders.length > 0" class="orders-mobile-page__list">
|
||||
<button
|
||||
v-for="order in orders"
|
||||
:key="order.id"
|
||||
type="button"
|
||||
class="order-card"
|
||||
@click="emit('select', order.id)"
|
||||
>
|
||||
<div class="order-card__content">
|
||||
<span class="order-card__label">سفارش شماره</span>
|
||||
<strong class="order-card__number">{{ order.orderNumber }}</strong>
|
||||
<p class="order-card__company">{{ order.companyName }}</p>
|
||||
<p v-if="order.finalAmount" class="order-card__amount">
|
||||
{{ formatCurrency(order.finalAmount) }}
|
||||
</p>
|
||||
<StatusBadge :status="order.status" class="order-card__badge" />
|
||||
</div>
|
||||
<span class="order-card__icon" :class="`order-card__icon--${iconVariant(order)}`">
|
||||
<IconFamBaftLogo v-if="companySlug(order) === 'fam-baft'" :size="36" />
|
||||
<IconParLogo v-else :size="36" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<EmptyState v-else message="سفارشی یافت نشد">
|
||||
<BaseButton @click="emit('back')">ثبت سفارش جدید</BaseButton>
|
||||
</EmptyState>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.orders-mobile-page {
|
||||
background: #ffffff;
|
||||
padding: 12px 0 0;
|
||||
}
|
||||
|
||||
.orders-mobile-page__nav {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 40px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.orders-mobile-page__title {
|
||||
margin: 0;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-btn--ghost {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.nav-btn__flip {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.orders-mobile-page__tabs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.orders-mobile-page__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 12px rgba(15, 23, 42, 0.06);
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.order-card__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.order-card__label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.order-card__number {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.order-card__company {
|
||||
margin: 0 0 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.order-card__amount {
|
||||
margin: 0 0 10px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.order-card__badge {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.order-card__icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 14px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.order-card__icon--par {
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.order-card__icon--fam-baft {
|
||||
background: #f3ece3;
|
||||
}
|
||||
|
||||
.order-card__icon--success {
|
||||
background: var(--color-success-soft);
|
||||
color: var(--color-success);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useProductOrdering } from '@/composables/useProductOrdering'
|
||||
import MobileProductPage from '@/views/products/components/MobileProductPage.vue'
|
||||
import DesktopProductPage from '@/views/products/components/DesktopProductPage.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
companyId,
|
||||
catalog,
|
||||
company,
|
||||
colorFilter,
|
||||
colors,
|
||||
colorLabels,
|
||||
products,
|
||||
meterages,
|
||||
errors,
|
||||
toast,
|
||||
cartStore,
|
||||
adjustMeterage,
|
||||
setMeterage,
|
||||
addProduct,
|
||||
goToCart,
|
||||
goBack,
|
||||
} = useProductOrdering()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MobileProductPage
|
||||
class="mobile-only"
|
||||
:catalog-code="catalog?.code ?? ''"
|
||||
:company-name="company?.name ?? ''"
|
||||
:colors="colors"
|
||||
:color-labels="colorLabels"
|
||||
:color-filter="colorFilter"
|
||||
:products="products"
|
||||
:meterages="meterages"
|
||||
:errors="errors"
|
||||
:total-meterage="cartStore.totalMeterage"
|
||||
:item-count="cartStore.itemCount"
|
||||
:toast="toast"
|
||||
@back="goBack"
|
||||
@update:color-filter="colorFilter = $event"
|
||||
@adjust="adjustMeterage"
|
||||
@set-meterage="setMeterage"
|
||||
@add="addProduct"
|
||||
@cart="goToCart"
|
||||
/>
|
||||
|
||||
<DesktopProductPage
|
||||
class="desktop-only"
|
||||
:catalog-code="catalog?.code ?? ''"
|
||||
:company-name="company?.name ?? ''"
|
||||
:company-id="companyId"
|
||||
:colors="colors"
|
||||
:color-labels="colorLabels"
|
||||
:color-filter="colorFilter"
|
||||
:products="products"
|
||||
:meterages="meterages"
|
||||
:errors="errors"
|
||||
:item-count="cartStore.itemCount"
|
||||
:total-meterage="cartStore.totalMeterage"
|
||||
:remaining-capacity="cartStore.remainingCapacity"
|
||||
:toast="toast"
|
||||
@update:color-filter="colorFilter = $event"
|
||||
@adjust="adjustMeterage"
|
||||
@set-meterage="setMeterage"
|
||||
@add="addProduct"
|
||||
@cart="goToCart"
|
||||
@home="router.push('/')"
|
||||
@catalogs="goBack"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,303 @@
|
||||
<script setup lang="ts">
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import IconCart from '@/components/icons/IconCart.vue'
|
||||
import { formatMeterage, formatNumber } from '@/utils/format'
|
||||
import type { Product } from '@/types'
|
||||
|
||||
defineProps<{
|
||||
catalogCode: string
|
||||
companyName: string
|
||||
companyId: string
|
||||
colors: string[]
|
||||
colorLabels: Record<string, string>
|
||||
colorFilter: string
|
||||
products: Product[]
|
||||
meterages: Record<string, number>
|
||||
errors: Record<string, string>
|
||||
itemCount: number
|
||||
totalMeterage: number
|
||||
remainingCapacity: number
|
||||
toast: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:colorFilter': [value: string]
|
||||
adjust: [productId: string, delta: number, step: number]
|
||||
setMeterage: [productId: string, value: number]
|
||||
add: [productId: string]
|
||||
cart: []
|
||||
home: []
|
||||
catalogs: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-page product-desktop-page">
|
||||
<header class="desktop-page__head">
|
||||
<h1 class="desktop-page__title">کالیته {{ catalogCode }}</h1>
|
||||
<p class="desktop-page__subtitle">{{ companyName }}</p>
|
||||
</header>
|
||||
|
||||
<nav class="product-breadcrumbs" aria-label="مسیر">
|
||||
<button type="button" @click="emit('home')">سفارشگذاری</button>
|
||||
<span aria-hidden="true">›</span>
|
||||
<button type="button" @click="emit('catalogs')">{{ companyName }}</button>
|
||||
<span aria-hidden="true">›</span>
|
||||
<span>کالیته {{ catalogCode }}</span>
|
||||
</nav>
|
||||
|
||||
<div class="desktop-two-col">
|
||||
<div class="desktop-two-col__main">
|
||||
<div class="color-tabs">
|
||||
<button
|
||||
v-for="color in colors"
|
||||
:key="color"
|
||||
type="button"
|
||||
class="color-tab"
|
||||
:class="{ active: colorFilter === color }"
|
||||
@click="emit('update:colorFilter', color)"
|
||||
>
|
||||
{{ colorLabels[color] ?? color }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="product-desktop-grid">
|
||||
<article v-for="product in products" :key="product.id" class="product-desktop-card">
|
||||
<div class="product-desktop-card__preview" aria-hidden="true" />
|
||||
<div class="product-desktop-card__body">
|
||||
<div class="product-desktop-card__head">
|
||||
<h3>{{ product.name }}</h3>
|
||||
<span>کد: {{ product.code }}</span>
|
||||
</div>
|
||||
<p class="product-desktop-card__meta">
|
||||
حداقل سفارش: {{ formatMeterage(product.minMeterage) }}
|
||||
</p>
|
||||
<div class="meterage-input-row">
|
||||
<button
|
||||
type="button"
|
||||
class="qty-btn"
|
||||
aria-label="کاهش"
|
||||
@click="emit('adjust', product.id, -1, product.orderMultiplier)"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
:value="meterages[product.id] ?? ''"
|
||||
type="number"
|
||||
class="meterage-input"
|
||||
:step="product.orderMultiplier"
|
||||
:min="0"
|
||||
placeholder="۰"
|
||||
@input="
|
||||
emit(
|
||||
'setMeterage',
|
||||
product.id,
|
||||
Number(($event.target as HTMLInputElement).value) || 0,
|
||||
)
|
||||
"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="qty-btn"
|
||||
aria-label="افزایش"
|
||||
@click="emit('adjust', product.id, 1, product.orderMultiplier)"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<BaseButton class="desktop-btn-compact product-desktop-card__add" @click="emit('add', product.id)">
|
||||
<IconCart :size="16" />
|
||||
افزودن
|
||||
</BaseButton>
|
||||
<p v-if="errors[product.id]" class="error-text">{{ errors[product.id] }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside v-if="itemCount > 0" class="desktop-summary-panel">
|
||||
<h2 class="desktop-summary-panel__title">خلاصه سبد</h2>
|
||||
<div class="desktop-summary-panel__row">
|
||||
<span>تعداد اقلام</span>
|
||||
<strong>{{ formatNumber(itemCount) }}</strong>
|
||||
</div>
|
||||
<div class="desktop-summary-panel__row">
|
||||
<span>کل متراژ</span>
|
||||
<strong>{{ formatMeterage(totalMeterage) }}</strong>
|
||||
</div>
|
||||
<div class="desktop-summary-panel__row">
|
||||
<span>باقیمانده</span>
|
||||
<strong>{{ formatMeterage(remainingCapacity) }}</strong>
|
||||
</div>
|
||||
<div class="desktop-summary-panel__action">
|
||||
<BaseButton block @click="emit('cart')">
|
||||
<IconCart :size="18" />
|
||||
مشاهده سبد خرید
|
||||
</BaseButton>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div v-if="toast" class="product-desktop-toast">{{ toast }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.product-breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.product-breadcrumbs button {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.color-tab {
|
||||
height: 34px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 999px;
|
||||
background: var(--color-surface);
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-tab.active {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.product-desktop-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.product-desktop-grid {
|
||||
grid-template-columns: repeat(2, minmax(280px, 420px));
|
||||
}
|
||||
}
|
||||
|
||||
.product-desktop-card {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--shadow-sm);
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.product-desktop-card__preview {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: linear-gradient(135deg, #e2e8f0, #cbd5e1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.product-desktop-card__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.product-desktop-card__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.product-desktop-card__head h3 {
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.product-desktop-card__head span {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.product-desktop-card__meta {
|
||||
margin: 0 0 10px;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.meterage-input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.qty-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.meterage-input {
|
||||
width: 72px;
|
||||
height: 32px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.product-desktop-card__add :deep(.btn) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 36px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: var(--color-danger);
|
||||
font-size: 0.75rem;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
.product-desktop-toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-text);
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
z-index: 150;
|
||||
}
|
||||
|
||||
.desktop-summary-panel__action :deep(.btn) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,227 @@
|
||||
<script setup lang="ts">
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import CartMeterageFooter from '@/components/ui/CartMeterageFooter.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import { formatMeterage } from '@/utils/format'
|
||||
import type { Product } from '@/types'
|
||||
|
||||
defineProps<{
|
||||
catalogCode: string
|
||||
companyName: string
|
||||
colors: string[]
|
||||
colorLabels: Record<string, string>
|
||||
colorFilter: string
|
||||
products: Product[]
|
||||
meterages: Record<string, number>
|
||||
errors: Record<string, string>
|
||||
totalMeterage: number
|
||||
itemCount: number
|
||||
toast: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
back: []
|
||||
'update:colorFilter': [value: string]
|
||||
adjust: [productId: string, delta: number, step: number]
|
||||
setMeterage: [productId: string, value: number]
|
||||
add: [productId: string]
|
||||
cart: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page product-mobile-page">
|
||||
<PageHeader
|
||||
:title="`کالیته ${catalogCode}`"
|
||||
:subtitle="companyName"
|
||||
show-back
|
||||
centered
|
||||
@back="emit('back')"
|
||||
/>
|
||||
|
||||
<div class="color-tabs">
|
||||
<button
|
||||
v-for="color in colors"
|
||||
:key="color"
|
||||
type="button"
|
||||
class="color-tab"
|
||||
:class="{ active: colorFilter === color }"
|
||||
@click="emit('update:colorFilter', color)"
|
||||
>
|
||||
{{ colorLabels[color] ?? color }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="page__scroll product-mobile-page__scroll">
|
||||
<div class="product-mobile-list">
|
||||
<article v-for="product in products" :key="product.id" class="product-mobile-card">
|
||||
<div class="product-mobile-card__head">
|
||||
<h3>{{ product.name }}</h3>
|
||||
<span>کد: {{ product.code }}</span>
|
||||
</div>
|
||||
<p class="product-mobile-card__meta">حداقل سفارش: {{ formatMeterage(product.minMeterage) }}</p>
|
||||
<div class="meterage-input-row">
|
||||
<button
|
||||
type="button"
|
||||
class="qty-btn"
|
||||
aria-label="کاهش"
|
||||
@click="emit('adjust', product.id, -1, product.orderMultiplier)"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
:value="meterages[product.id] ?? ''"
|
||||
type="number"
|
||||
class="meterage-input"
|
||||
:step="product.orderMultiplier"
|
||||
:min="0"
|
||||
placeholder="۰"
|
||||
@input="
|
||||
emit(
|
||||
'setMeterage',
|
||||
product.id,
|
||||
Number(($event.target as HTMLInputElement).value) || 0,
|
||||
)
|
||||
"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="qty-btn"
|
||||
aria-label="افزایش"
|
||||
@click="emit('adjust', product.id, 1, product.orderMultiplier)"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<BaseButton block @click="emit('add', product.id)">افزودن</BaseButton>
|
||||
<p v-if="errors[product.id]" class="error-text">{{ errors[product.id] }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="toast" class="toast">{{ toast }}</div>
|
||||
|
||||
<div v-if="itemCount > 0" class="sticky-footer">
|
||||
<CartMeterageFooter
|
||||
:total-meterage="totalMeterage"
|
||||
:item-count="itemCount"
|
||||
@click="emit('cart')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.product-mobile-page {
|
||||
background: #fff;
|
||||
padding: 12px 0 0;
|
||||
}
|
||||
|
||||
.product-mobile-page__scroll {
|
||||
padding-bottom: 76px;
|
||||
}
|
||||
|
||||
.color-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.color-tab {
|
||||
flex-shrink: 0;
|
||||
height: 34px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 999px;
|
||||
background: var(--color-surface);
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-tab.active {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.product-mobile-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.product-mobile-card {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.product-mobile-card__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.product-mobile-card__head h3 {
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.product-mobile-card__head span {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.product-mobile-card__meta {
|
||||
margin: 0 0 12px;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.meterage-input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.qty-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.meterage-input {
|
||||
width: 88px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: var(--color-danger);
|
||||
font-size: 0.8125rem;
|
||||
margin: 8px 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: calc(var(--nav-height) + 72px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-text);
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
z-index: 150;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import CheckListView from '@/views/checkout/CheckListView.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CheckListView />
|
||||
</template>
|
||||
@@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import BaseInput from '@/components/ui/BaseInput.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import {
|
||||
FORM_VALIDATION_SUMMARY,
|
||||
clearFieldError,
|
||||
hasFieldErrors,
|
||||
type FieldErrors,
|
||||
} from '@/utils/formErrors'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const firstName = ref(auth.user?.firstName ?? '')
|
||||
const lastName = ref(auth.user?.lastName ?? '')
|
||||
const nationalId = ref(auth.user?.nationalId ?? '')
|
||||
const fieldErrors = ref<FieldErrors>({})
|
||||
const formError = ref('')
|
||||
const saved = ref(false)
|
||||
|
||||
function clearField(key: string) {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, key)
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
}
|
||||
|
||||
function save() {
|
||||
fieldErrors.value = {}
|
||||
formError.value = ''
|
||||
saved.value = false
|
||||
|
||||
const errors: FieldErrors = {}
|
||||
if (!firstName.value.trim()) errors.firstName = 'نام الزامی است.'
|
||||
if (!lastName.value.trim()) errors.lastName = 'نام خانوادگی الزامی است.'
|
||||
if (nationalId.value && nationalId.value.length !== 10) {
|
||||
errors.nationalId = 'کد ملی باید ۱۰ رقم باشد.'
|
||||
}
|
||||
|
||||
if (hasFieldErrors(errors)) {
|
||||
fieldErrors.value = errors
|
||||
formError.value = FORM_VALIDATION_SUMMARY
|
||||
return
|
||||
}
|
||||
|
||||
auth.updateProfile({
|
||||
firstName: firstName.value.trim(),
|
||||
lastName: lastName.value.trim(),
|
||||
nationalId: nationalId.value || undefined,
|
||||
})
|
||||
saved.value = true
|
||||
setTimeout(() => {
|
||||
saved.value = false
|
||||
}, 2000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page profile-edit-page">
|
||||
<header class="profile-edit-page__nav">
|
||||
<button type="button" class="nav-btn" aria-label="بازگشت" @click="router.push('/profile')">
|
||||
<IconChevronBack :size="20" />
|
||||
</button>
|
||||
<h1 class="profile-edit-page__title">اطلاعات کاربری</h1>
|
||||
<button type="button" class="nav-btn nav-btn--ghost" aria-hidden="true" tabindex="-1">
|
||||
<IconChevronBack :size="18" class="nav-btn__flip" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="page__scroll page__scroll--with-nav">
|
||||
<div class="profile-form card">
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<BaseInput
|
||||
v-model="firstName"
|
||||
label="نام"
|
||||
:error="fieldErrors.firstName"
|
||||
@update:model-value="clearField('firstName')"
|
||||
/>
|
||||
<BaseInput
|
||||
v-model="lastName"
|
||||
label="نام خانوادگی"
|
||||
:error="fieldErrors.lastName"
|
||||
@update:model-value="clearField('lastName')"
|
||||
/>
|
||||
<BaseInput
|
||||
v-model="nationalId"
|
||||
label="کد ملی"
|
||||
maxlength="10"
|
||||
hint="برای ثبت چک الزامی است"
|
||||
:error="fieldErrors.nationalId"
|
||||
@update:model-value="clearField('nationalId')"
|
||||
/>
|
||||
<BaseButton block @click="save">ذخیره تغییرات</BaseButton>
|
||||
<p v-if="saved" class="profile-form__saved">ذخیره شد ✓</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.profile-edit-page {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.profile-edit-page__nav {
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 40px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.profile-edit-page__title {
|
||||
margin: 0;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-btn--ghost {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-btn__flip {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.profile-form__saved {
|
||||
margin: 8px 0 0;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
color: var(--color-success);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import MobileProfilePage from '@/views/profile/components/MobileProfilePage.vue'
|
||||
import DesktopProfilePage from '@/views/profile/components/DesktopProfilePage.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
function logout() {
|
||||
auth.logout()
|
||||
router.replace('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MobileProfilePage class="mobile-only" @logout="logout" />
|
||||
<DesktopProfilePage class="desktop-only" @logout="logout" />
|
||||
</template>
|
||||
@@ -0,0 +1,723 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconEditPen from '@/components/icons/IconEditPen.vue'
|
||||
import IconOrders from '@/components/icons/IconOrders.vue'
|
||||
import IconCheckBook from '@/components/icons/IconCheckBook.vue'
|
||||
import IconCash from '@/components/icons/IconCash.vue'
|
||||
import IconBuilding from '@/components/icons/IconBuilding.vue'
|
||||
import IconBolt from '@/components/icons/IconBolt.vue'
|
||||
import IconSettings from '@/components/icons/IconSettings.vue'
|
||||
import IconProfile from '@/components/icons/IconProfile.vue'
|
||||
import IconBell from '@/components/icons/IconBell.vue'
|
||||
import IconMapPin from '@/components/icons/IconMapPin.vue'
|
||||
import IconHeadset from '@/components/icons/IconHeadset.vue'
|
||||
import IconCashSettlement from '@/components/icons/IconCashSettlement.vue'
|
||||
import IconPhone from '@/components/icons/IconPhone.vue'
|
||||
import IconMail from '@/components/icons/IconMail.vue'
|
||||
import IconVerified from '@/components/icons/IconVerified.vue'
|
||||
import IconLayoutGrid from '@/components/icons/IconLayoutGrid.vue'
|
||||
import IconLock from '@/components/icons/IconLock.vue'
|
||||
import IconIdCard from '@/components/icons/IconIdCard.vue'
|
||||
import { formatCurrency, formatDate, formatNumber, toPersianDigits } from '@/utils/format'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const emit = defineEmits<{ logout: [] }>()
|
||||
|
||||
const initials = computed(() => {
|
||||
const first = auth.user?.firstName?.[0] ?? ''
|
||||
const last = auth.user?.lastName?.[0] ?? ''
|
||||
return `${first}${last}` || '؟'
|
||||
})
|
||||
|
||||
const ordersCount = computed(() => {
|
||||
if (!auth.user) return 0
|
||||
return orderStore.userOrders(auth.user.id).length
|
||||
})
|
||||
|
||||
const checksCount = computed(() => {
|
||||
if (!auth.user) return 0
|
||||
const submitted = orderStore
|
||||
.userOrders(auth.user.id)
|
||||
.reduce((sum, order) => sum + (order.checks?.length ?? 0), 0)
|
||||
return submitted + orderStore.draftChecks.length
|
||||
})
|
||||
|
||||
const customerCode = computed(() => {
|
||||
if (auth.user?.nationalId) return toPersianDigits(auth.user.nationalId.slice(-6).padStart(6, '0'))
|
||||
return toPersianDigits(auth.user?.id.slice(-6) ?? '—')
|
||||
})
|
||||
|
||||
const displayEmail = computed(() => {
|
||||
const phone = auth.user?.phone ?? ''
|
||||
if (!phone) return '—'
|
||||
return `user${phone.slice(-4)}@parsbaft.ir`
|
||||
})
|
||||
|
||||
const joinDate = computed(() =>
|
||||
auth.user?.createdAt ? formatDate(auth.user.createdAt) : '—',
|
||||
)
|
||||
|
||||
const creditBalance = computed(() => 2_450_000)
|
||||
|
||||
const companyRows = computed(() => [
|
||||
{ label: 'نام شرکت', value: auth.fullName || '—' },
|
||||
{ label: 'شناسه ملی', value: auth.user?.nationalId ? toPersianDigits(auth.user.nationalId) : '—' },
|
||||
{
|
||||
label: 'کد اقتصادی',
|
||||
value: auth.user?.nationalId ? toPersianDigits(`4${auth.user.nationalId.slice(0, 10)}`) : '—',
|
||||
},
|
||||
{ label: 'تاریخ عضویت', value: joinDate.value },
|
||||
{ label: 'نوع مشتری', value: 'مشتری عمده', badge: true },
|
||||
{ label: 'وضعیت حساب', value: 'فعال', badge: true },
|
||||
])
|
||||
|
||||
const quickAccess = [
|
||||
{ label: 'حسابهای بانکی', icon: IconCashSettlement },
|
||||
{ label: 'آدرسها', icon: IconMapPin },
|
||||
{ label: 'حمایت و پشتیبانی', icon: IconHeadset, href: 'tel:02112345678' },
|
||||
{ label: 'فاکتورهای من', icon: IconOrders, path: '/orders' },
|
||||
]
|
||||
|
||||
const settingsItems: Array<{
|
||||
id: string
|
||||
label: string
|
||||
description: string
|
||||
icon: Component
|
||||
path?: string
|
||||
action?: 'logout'
|
||||
}> = [
|
||||
{
|
||||
id: 'profile',
|
||||
label: 'اطلاعات کاربری',
|
||||
description: 'ویرایش نام، موبایل و ایمیل',
|
||||
icon: IconProfile,
|
||||
path: '/profile/edit',
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
label: 'امنیت حساب',
|
||||
description: 'تغییر رمز عبور و تنظیمات ورود',
|
||||
icon: IconLock,
|
||||
path: '/profile/edit',
|
||||
},
|
||||
{
|
||||
id: 'notifications',
|
||||
label: 'اعلانها',
|
||||
description: 'مدیریت اعلانهای ایمیل و پیامک',
|
||||
icon: IconBell,
|
||||
path: '/profile/edit',
|
||||
},
|
||||
{
|
||||
id: 'display',
|
||||
label: 'تنظیمات نمایش',
|
||||
description: 'زبان، واحد پول و حالت نمایش',
|
||||
icon: IconLayoutGrid,
|
||||
path: '/profile/edit',
|
||||
},
|
||||
]
|
||||
|
||||
function handleQuick(item: (typeof quickAccess)[number]) {
|
||||
if (item.path) router.push(item.path)
|
||||
else if (item.href) window.location.href = item.href
|
||||
}
|
||||
|
||||
function handleSetting(item: (typeof settingsItems)[number]) {
|
||||
if (item.action === 'logout') {
|
||||
emit('logout')
|
||||
return
|
||||
}
|
||||
if (item.path) router.push(item.path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-page desktop-profile">
|
||||
<header class="desktop-profile__head">
|
||||
<h1 class="desktop-profile__title">پروفایل</h1>
|
||||
<p class="desktop-profile__subtitle">مدیریت اطلاعات حساب کاربری و تنظیمات</p>
|
||||
</header>
|
||||
|
||||
<section class="profile-overview">
|
||||
<div class="profile-overview__identity">
|
||||
<div class="profile-overview__aside">
|
||||
<div class="profile-overview__avatar">{{ initials }}</div>
|
||||
<button
|
||||
type="button"
|
||||
class="profile-overview__edit"
|
||||
@click="router.push('/profile/edit')"
|
||||
>
|
||||
<IconEditPen :size="16" />
|
||||
ویرایش اطلاعات
|
||||
</button>
|
||||
</div>
|
||||
<div class="profile-overview__details">
|
||||
<div class="profile-overview__name-row">
|
||||
<h2 class="profile-overview__name">{{ auth.fullName }}</h2>
|
||||
<span class="profile-overview__verified" title="حساب تأیید شده">
|
||||
<IconVerified :size="18" />
|
||||
</span>
|
||||
</div>
|
||||
<p class="profile-overview__meta">
|
||||
<IconIdCard :size="15" class="profile-overview__meta-icon" />
|
||||
<span class="profile-overview__meta-label">کد مشتری:</span>
|
||||
<span>{{ customerCode }}</span>
|
||||
</p>
|
||||
<p class="profile-overview__meta">
|
||||
<IconPhone :size="15" class="profile-overview__meta-icon" />
|
||||
<span dir="ltr">{{ auth.user?.phone }}</span>
|
||||
</p>
|
||||
<p class="profile-overview__meta">
|
||||
<IconMail :size="15" class="profile-overview__meta-icon" />
|
||||
<span dir="ltr">{{ displayEmail }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-overview__stats">
|
||||
<article class="profile-stat">
|
||||
<span class="profile-stat__icon profile-stat__icon--amber">
|
||||
<IconCash :size="20" />
|
||||
</span>
|
||||
<strong class="profile-stat__value profile-stat__value--sm">
|
||||
{{ formatCurrency(creditBalance) }}
|
||||
</strong>
|
||||
<span class="profile-stat__label">موجودی اعتبار</span>
|
||||
<button type="button" class="profile-stat__link">مشاهده جزئیات</button>
|
||||
</article>
|
||||
|
||||
<article class="profile-stat">
|
||||
<span class="profile-stat__icon profile-stat__icon--green">
|
||||
<IconCheckBook :size="20" />
|
||||
</span>
|
||||
<strong class="profile-stat__value">{{ formatNumber(checksCount) }}</strong>
|
||||
<span class="profile-stat__label">چک فعال</span>
|
||||
<button type="button" class="profile-stat__link" @click="router.push('/profile/checks')">
|
||||
مشاهده همه
|
||||
</button>
|
||||
</article>
|
||||
|
||||
<article class="profile-stat">
|
||||
<span class="profile-stat__icon profile-stat__icon--blue">
|
||||
<IconOrders :size="20" />
|
||||
</span>
|
||||
<strong class="profile-stat__value">{{ formatNumber(ordersCount) }}</strong>
|
||||
<span class="profile-stat__label">سفارش ثبتشده</span>
|
||||
<button type="button" class="profile-stat__link" @click="router.push('/orders')">
|
||||
مشاهده همه
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="desktop-profile__grid">
|
||||
<section class="profile-panel profile-panel--settings">
|
||||
<h3 class="profile-panel__title">
|
||||
<IconSettings :size="18" />
|
||||
تنظیمات حساب
|
||||
</h3>
|
||||
<nav class="profile-settings" aria-label="تنظیمات حساب">
|
||||
<button
|
||||
v-for="item in settingsItems"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="profile-settings__item"
|
||||
@click="handleSetting(item)"
|
||||
>
|
||||
<span class="profile-settings__icon">
|
||||
<component :is="item.icon" :size="20" />
|
||||
</span>
|
||||
<span class="profile-settings__body">
|
||||
<strong class="profile-settings__label">{{ item.label }}</strong>
|
||||
<span class="profile-settings__desc">{{ item.description }}</span>
|
||||
</span>
|
||||
<IconChevronLeft :size="16" class="profile-settings__chevron" aria-hidden="true" />
|
||||
</button>
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
<section class="profile-panel profile-panel--quick">
|
||||
<h3 class="profile-panel__title">
|
||||
<IconBolt :size="18" />
|
||||
دسترسی سریع
|
||||
</h3>
|
||||
<div class="profile-quick-grid">
|
||||
<button
|
||||
v-for="item in quickAccess"
|
||||
:key="item.label"
|
||||
type="button"
|
||||
class="profile-quick-grid__item"
|
||||
@click="handleQuick(item)"
|
||||
>
|
||||
<span class="profile-quick-grid__icon">
|
||||
<component :is="item.icon" :size="22" />
|
||||
</span>
|
||||
<span class="profile-quick-grid__label">{{ item.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="profile-panel profile-panel--company">
|
||||
<h3 class="profile-panel__title">
|
||||
<IconBuilding :size="18" />
|
||||
اطلاعات شرکت
|
||||
</h3>
|
||||
<dl class="profile-info-list">
|
||||
<div
|
||||
v-for="row in companyRows"
|
||||
:key="row.label"
|
||||
class="profile-info-list__row"
|
||||
>
|
||||
<dt>{{ row.label }}</dt>
|
||||
<dd>
|
||||
<span v-if="row.badge" class="profile-badge">{{ row.value }}</span>
|
||||
<template v-else>{{ row.value }}</template>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desktop-profile {
|
||||
max-width: 980px;
|
||||
margin-inline: auto;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.desktop-profile__head {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.desktop-profile__title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.desktop-profile__subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.profile-overview {
|
||||
direction: rtl;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
padding: 22px 24px;
|
||||
margin-bottom: 16px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.profile-overview__identity {
|
||||
flex: 1.1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 18px;
|
||||
padding-inline-end: 24px;
|
||||
border-inline-end: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.profile-overview__aside {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-overview__avatar {
|
||||
width: 92px;
|
||||
height: 92px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.profile-overview__edit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 132px;
|
||||
padding: 9px 14px;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: var(--radius-md);
|
||||
background: #fff;
|
||||
color: var(--color-primary);
|
||||
font-family: inherit;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.profile-overview__edit:hover {
|
||||
background: var(--color-primary-soft);
|
||||
}
|
||||
|
||||
.profile-overview__details {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.profile-overview__name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.profile-overview__name {
|
||||
margin: 0;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.profile-overview__verified {
|
||||
color: var(--color-success);
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-overview__meta {
|
||||
margin: 0 0 8px;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.profile-overview__meta-icon {
|
||||
color: #94a3b8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-overview__meta-label {
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.profile-overview__stats {
|
||||
direction: ltr;
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.profile-stat {
|
||||
direction: rtl;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 12px 14px;
|
||||
border-inline-start: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.profile-stat:first-child {
|
||||
border-inline-start: none;
|
||||
}
|
||||
|
||||
.profile-stat__icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.profile-stat__icon--blue {
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.profile-stat__icon--green {
|
||||
background: var(--color-success-soft);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.profile-stat__icon--amber {
|
||||
background: #fff6e5;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.profile-stat__value {
|
||||
font-size: 1.375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 4px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.profile-stat__value--sm {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.profile-stat__label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.profile-stat__link {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: var(--color-primary);
|
||||
font-family: inherit;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.profile-stat__link:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.desktop-profile__grid {
|
||||
direction: ltr;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.profile-panel {
|
||||
direction: rtl;
|
||||
padding: 18px 18px 16px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.profile-panel__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 14px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.profile-panel__title svg {
|
||||
color: var(--color-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-info-list {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile-info-list__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 11px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.profile-info-list__row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.profile-info-list__row dt {
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.profile-info-list__row dd {
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.profile-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-success-soft);
|
||||
color: var(--color-success);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.profile-quick-grid {
|
||||
direction: ltr;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.profile-quick-grid__item {
|
||||
direction: rtl;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
min-height: 108px;
|
||||
padding: 16px 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: #f8fafc;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.profile-quick-grid__item:hover {
|
||||
border-color: #c7d7ff;
|
||||
background: var(--color-primary-soft);
|
||||
}
|
||||
|
||||
.profile-quick-grid__icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
color: var(--color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.profile-quick-grid__label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.profile-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.profile-settings__item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 14px 4px;
|
||||
border: none;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
font-family: inherit;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.profile-settings__item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.profile-settings__item:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.profile-settings__icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-settings__body {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.profile-settings__label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.profile-settings__desc {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.profile-settings__chevron {
|
||||
color: #cbd5e1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.profile-overview {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.profile-overview__identity {
|
||||
border-inline-end: none;
|
||||
padding-inline-end: 0;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.profile-overview__stats {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.profile-stat:first-child {
|
||||
border-inline-start: none;
|
||||
}
|
||||
|
||||
.desktop-profile__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,224 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconProfile from '@/components/icons/IconProfile.vue'
|
||||
import IconCheckBook from '@/components/icons/IconCheckBook.vue'
|
||||
import IconOrders from '@/components/icons/IconOrders.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const initials = computed(() => {
|
||||
const first = auth.user?.firstName?.[0] ?? ''
|
||||
const last = auth.user?.lastName?.[0] ?? ''
|
||||
return `${first}${last}` || '؟'
|
||||
})
|
||||
|
||||
const draftCheckCount = computed(() => orderStore.draftChecks.length)
|
||||
|
||||
const menuItems = computed(() => [
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'اطلاعات کاربری',
|
||||
description: 'نام، نام خانوادگی و کد ملی',
|
||||
path: '/profile/edit',
|
||||
icon: IconProfile,
|
||||
},
|
||||
{
|
||||
id: 'checks',
|
||||
label: 'لیست چکها',
|
||||
description: 'چکهای ثبتشده و رأس چک',
|
||||
path: '/profile/checks',
|
||||
icon: IconCheckBook,
|
||||
badge: draftCheckCount.value,
|
||||
},
|
||||
{
|
||||
id: 'orders',
|
||||
label: 'سفارشهای من',
|
||||
description: 'پیگیری وضعیت سفارشها',
|
||||
path: '/orders',
|
||||
icon: IconOrders,
|
||||
},
|
||||
])
|
||||
|
||||
const emit = defineEmits<{ logout: [] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page profile-page">
|
||||
<div class="profile-hero">
|
||||
<div class="profile-hero__avatar">{{ initials }}</div>
|
||||
<h1 class="profile-hero__name">{{ auth.fullName }}</h1>
|
||||
<p class="profile-hero__phone" dir="ltr">{{ auth.user?.phone }}</p>
|
||||
</div>
|
||||
|
||||
<div class="page__scroll page__scroll--with-nav profile-page__scroll">
|
||||
<nav class="profile-menu" aria-label="منوی پروفایل">
|
||||
<button
|
||||
v-for="item in menuItems"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="profile-menu__item"
|
||||
@click="router.push(item.path)"
|
||||
>
|
||||
<span class="profile-menu__icon">
|
||||
<component :is="item.icon" :size="22" />
|
||||
</span>
|
||||
<span class="profile-menu__content">
|
||||
<strong class="profile-menu__label">{{ item.label }}</strong>
|
||||
<span class="profile-menu__desc">{{ item.description }}</span>
|
||||
</span>
|
||||
<span v-if="item.badge" class="profile-menu__badge">{{ item.badge }}</span>
|
||||
<IconChevronLeft :size="18" class="profile-menu__chevron" />
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<button type="button" class="profile-logout" @click="emit('logout')">خروج از حساب</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.profile-page {
|
||||
overflow: hidden;
|
||||
padding-top: 0;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.profile-hero {
|
||||
flex-shrink: 0;
|
||||
margin: 0 calc(-1 * var(--page-padding));
|
||||
padding: calc(28px + env(safe-area-inset-top, 0)) var(--page-padding) 32px;
|
||||
background: linear-gradient(160deg, var(--color-primary) 0%, #2a4a73 100%);
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.profile-hero__avatar {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin: 0 auto 12px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
border: 2px solid rgba(255, 255, 255, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.profile-hero__name {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.profile-hero__phone {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.profile-page__scroll {
|
||||
margin-top: -18px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.profile-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.profile-menu__item {
|
||||
display: grid;
|
||||
grid-template-columns: 44px 1fr auto auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--shadow-sm);
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
transition: box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.profile-menu__item:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.profile-menu__icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.profile-menu__content {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.profile-menu__label {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.profile-menu__desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.profile-menu__badge {
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-danger);
|
||||
color: #fff;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.profile-menu__chevron {
|
||||
color: var(--color-text-muted);
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.profile-logout {
|
||||
width: 100%;
|
||||
height: var(--btn-height);
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
color: var(--color-danger);
|
||||
font-family: inherit;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.profile-logout:hover {
|
||||
background: var(--color-danger-light);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user