Polish customer app UI and streamline CRM overlays.
Replace standalone customer create and meeting report pages with overlays, add TimeInput, remove opaque input backgrounds app-wide, improve desktop layouts/skeletons, and refresh shopping, customer, and reminder screens.
This commit is contained in:
@@ -90,7 +90,7 @@ onMounted(async () => {
|
||||
<template>
|
||||
<div class="page login-page">
|
||||
<img class="login-page__bg" :src="fabricUrl" alt="" />
|
||||
<img class="login-page__fabric" :src="fabricUrl2" alt="" />
|
||||
<!-- <img class="login-page__fabric" :src="fabricUrl2" alt="" />-->
|
||||
|
||||
<div class="login-page__scroll">
|
||||
<div class="login-page__stage">
|
||||
@@ -346,7 +346,6 @@ onMounted(async () => {
|
||||
padding: 0 8px 0 14px;
|
||||
border: 1px solid #e4e0db;
|
||||
border-radius: 14px;
|
||||
background: #e4ddd4;
|
||||
}
|
||||
|
||||
.phone-field.is-invalid {
|
||||
|
||||
@@ -456,7 +456,6 @@ onMounted(() => {
|
||||
padding: 0;
|
||||
border: 1px solid #e4e0db;
|
||||
border-radius: 16px;
|
||||
background: #f7f5f1;
|
||||
color: #1c1b19;
|
||||
font-family: inherit;
|
||||
font-size: 1.25rem;
|
||||
@@ -469,7 +468,6 @@ onMounted(() => {
|
||||
.otp-box:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.otp-box--error {
|
||||
|
||||
@@ -385,7 +385,6 @@ async function submit() {
|
||||
padding: 0 14px;
|
||||
border: 1px solid #e0d9d0;
|
||||
border-radius: 14px;
|
||||
background: #ebe4dc;
|
||||
}
|
||||
|
||||
.reg-field.is-invalid,
|
||||
|
||||
@@ -400,8 +400,8 @@ async function addAllToInquiry() {
|
||||
|
||||
<style scoped>
|
||||
.calite-detail {
|
||||
background: #f4f0ed;
|
||||
color: #2a2622;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.calite-detail__nav,
|
||||
@@ -563,7 +563,7 @@ async function addAllToInquiry() {
|
||||
top: 16%;
|
||||
bottom: 16%;
|
||||
width: 1px;
|
||||
background: #e4dfd8;
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.attr__icon {
|
||||
@@ -617,7 +617,7 @@ async function addAllToInquiry() {
|
||||
padding: 0 14px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: #ece8e2;
|
||||
background: var(--color-tab-inactive-bg);
|
||||
color: #6a645e;
|
||||
font: inherit;
|
||||
font-size: 0.8125rem;
|
||||
@@ -644,11 +644,12 @@ async function addAllToInquiry() {
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
color: #1c1c1c;
|
||||
box-shadow: 0 1px 3px rgba(40, 32, 24, 0.05);
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.pattern-card__photo {
|
||||
@@ -903,7 +904,7 @@ html.dark .calite-detail__footer.sticky-footer {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
padding: 12px 28px 24px;
|
||||
background: #f4f0ed;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.inquiry-bar {
|
||||
|
||||
@@ -175,7 +175,6 @@ async function submit() {
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
font: inherit;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@@ -193,7 +192,6 @@ async function submit() {
|
||||
padding: 10px 12px;
|
||||
font: inherit;
|
||||
resize: vertical;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconHeadset from '@/components/icons/IconHeadset.vue'
|
||||
import IconDiamond from '@/components/icons/IconDiamond.vue'
|
||||
import IconCatalog from '@/components/icons/IconCatalog.vue'
|
||||
import { categoryIconOf } from '@/utils/categoryIcons'
|
||||
import { categoryIconOf, usageCategoryIconKey } from '@/utils/categoryIcons'
|
||||
import { useGoBack } from '@/composables/useGoBack'
|
||||
import AuthThemeToggle from '@/components/ui/AuthThemeToggle.vue'
|
||||
import PageSkeleton from '@/components/ui/PageSkeleton.vue'
|
||||
@@ -23,10 +23,6 @@ const seasonCategories = ref<SeasonCategory[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
function usageIconKey(item: ProductCategory) {
|
||||
return item.icon || (item.id === 'shirt' ? 'id-card' : item.id === 'suit' ? 'bag' : 'grid')
|
||||
}
|
||||
|
||||
function seasonIconKey(item: SeasonCategory) {
|
||||
return item.icon || (item.id === 'summer' ? 'bolt' : item.id === 'winter' ? 'feather' : 'diamond')
|
||||
}
|
||||
@@ -91,7 +87,7 @@ function openSeason(id: string) {
|
||||
:style="item.imageUrl ? { backgroundImage: `url(${item.imageUrl})` } : undefined"
|
||||
>
|
||||
<span v-if="item.showIcon !== false" class="usage-card__icon">
|
||||
<component :is="categoryIconOf(usageIconKey(item), IconCatalog)" :size="18" />
|
||||
<component :is="categoryIconOf(usageCategoryIconKey(item), IconCatalog)" :size="18" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="usage-card__body">
|
||||
@@ -114,7 +110,7 @@ function openSeason(id: string) {
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="season-card"
|
||||
:class="`season-card--${item.id}`"
|
||||
:class="[`season-card--${item.id}`, { 'season-card--banner': !!item.imageUrl }]"
|
||||
:style="item.imageUrl ? { backgroundImage: `url(${item.imageUrl})` } : undefined"
|
||||
@click="openSeason(item.id)"
|
||||
>
|
||||
@@ -260,7 +256,8 @@ function openSeason(id: string) {
|
||||
margin: 0;
|
||||
line-height: normal;
|
||||
font-family: inherit;
|
||||
box-shadow: var(--shadow-sm);
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.usage-card__image {
|
||||
@@ -297,11 +294,12 @@ function openSeason(id: string) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-surface);
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.usage-card__body {
|
||||
@@ -335,9 +333,9 @@ function openSeason(id: string) {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
min-height: 124px;
|
||||
min-height: 148px;
|
||||
width: 100%;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
@@ -346,17 +344,28 @@ function openSeason(id: string) {
|
||||
overflow: hidden;
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
padding: 16px 18px;
|
||||
padding: 14px 16px;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
box-shadow: var(--shadow-sm);
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--color-border);
|
||||
background-color: #d6c3a8;
|
||||
background-image: linear-gradient(160deg, #d6c3a8, #8b7355);
|
||||
background-size: cover;
|
||||
background-position: center left;
|
||||
background-position: center top;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.season-card--banner {
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
min-height: 156px;
|
||||
aspect-ratio: 2.35 / 1;
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
}
|
||||
|
||||
.season-card--summer {
|
||||
background-image: linear-gradient(160deg, #f3e6c8, #c4a574);
|
||||
}
|
||||
@@ -369,25 +378,23 @@ function openSeason(id: string) {
|
||||
background-image: linear-gradient(160deg, #efe6d6, #b9a48a);
|
||||
}
|
||||
|
||||
.season-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
color-mix(in srgb, var(--color-surface) 90%, transparent) 0%,
|
||||
color-mix(in srgb, var(--color-surface) 72%, transparent) 38%,
|
||||
color-mix(in srgb, var(--color-surface) 22%, transparent) 68%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.season-card__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 58%;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.season-card--banner .season-card__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-self: stretch;
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
margin-top: auto;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.season-card__content strong {
|
||||
@@ -415,6 +422,19 @@ function openSeason(id: string) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.season-card--banner .season-card__content strong,
|
||||
.season-card--banner .season-card__content p,
|
||||
.season-card--banner .season-card__link {
|
||||
color: #fff;
|
||||
text-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.55),
|
||||
0 2px 12px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.season-card--banner .season-card__icon {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.season-card__content p {
|
||||
margin: 0 0 10px;
|
||||
color: var(--color-text-muted);
|
||||
|
||||
@@ -398,7 +398,7 @@ const currentStep = 1
|
||||
}
|
||||
|
||||
.company-showcase--par .company-showcase__cta {
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useCrmStore } from '@/stores/crm'
|
||||
import { useGoBack } from '@/composables/useGoBack'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
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 { validatePhone } from '@/utils/validation'
|
||||
import {
|
||||
FORM_VALIDATION_SUMMARY,
|
||||
clearFieldError,
|
||||
hasFieldErrors,
|
||||
type FieldErrors,
|
||||
} from '@/utils/formErrors'
|
||||
import { ApiError } from '@/services/http'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const crm = useCrmStore()
|
||||
const { goBack } = useGoBack('/customers')
|
||||
|
||||
const firstName = ref('')
|
||||
const lastName = ref('')
|
||||
const phone = ref('')
|
||||
const businessName = ref('')
|
||||
const fieldErrors = ref<FieldErrors>({})
|
||||
const formError = ref('')
|
||||
const saving = ref(false)
|
||||
|
||||
function clearField(key: string) {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, key)
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
fieldErrors.value = {}
|
||||
formError.value = ''
|
||||
|
||||
if (!auth.isAgent) {
|
||||
formError.value = 'فقط واسطه میتواند مشتری به لیست خود اضافه کند.'
|
||||
return
|
||||
}
|
||||
|
||||
const normalized = normalizePhone(phone.value)
|
||||
const errors: FieldErrors = {}
|
||||
if (!firstName.value.trim()) errors.firstName = 'نام الزامی است.'
|
||||
if (!lastName.value.trim()) errors.lastName = 'نام خانوادگی الزامی است.'
|
||||
if (!validatePhone(normalized)) errors.phone = 'شماره موبایل معتبر نیست.'
|
||||
|
||||
if (hasFieldErrors(errors)) {
|
||||
fieldErrors.value = errors
|
||||
formError.value = FORM_VALIDATION_SUMMARY
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const created = (await crm.registerCustomer({
|
||||
firstName: firstName.value.trim(),
|
||||
lastName: lastName.value.trim(),
|
||||
phone: normalized,
|
||||
businessName: businessName.value.trim() || undefined,
|
||||
})) as { id?: string }
|
||||
|
||||
if (created?.id) {
|
||||
await router.replace({ path: '/customer', query: { id: created.id } })
|
||||
return
|
||||
}
|
||||
await router.replace('/customers')
|
||||
} catch (e) {
|
||||
formError.value =
|
||||
e instanceof ApiError || e instanceof Error
|
||||
? e.message
|
||||
: 'ثبت مشتری ناموفق بود.'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page customer-create-page">
|
||||
<PageHeader title="افزودن مشتری" show-back centered @back="goBack" />
|
||||
|
||||
<div class="page__scroll customer-create-scroll">
|
||||
<p class="intro">
|
||||
مشتری به لیست شما اضافه میشود و بعد از ورود با همین شماره، سفارشهایش به حساب شما وصل میماند.
|
||||
</p>
|
||||
|
||||
<FormErrorBanner :message="formError" />
|
||||
|
||||
<div class="form-card">
|
||||
<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"
|
||||
inputmode="tel"
|
||||
dir="ltr"
|
||||
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
|
||||
:error="fieldErrors.phone"
|
||||
@update:model-value="clearField('phone')"
|
||||
/>
|
||||
<BaseInput
|
||||
v-model="businessName"
|
||||
label="نام فروشگاه یا برند (اختیاری)"
|
||||
placeholder="مثلاً بوتیک رز"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<BaseButton block :loading="saving" :disabled="saving" @click="submit">
|
||||
ثبت مشتری
|
||||
</BaseButton>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.customer-create-page {
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.customer-create-scroll {
|
||||
padding: 8px var(--page-padding) 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 16px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding: 12px var(--page-padding) calc(12px + env(safe-area-inset-bottom, 0px));
|
||||
background: color-mix(in srgb, var(--color-bg) 92%, transparent);
|
||||
}
|
||||
</style>
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
CrmCustomer,
|
||||
CrmMeeting,
|
||||
CrmOrderSummary,
|
||||
CustomerStatus,
|
||||
CustomerTab,
|
||||
MeetingInteractionType,
|
||||
} from '@/types/crm'
|
||||
@@ -21,6 +22,7 @@ import IconClipboardEmpty from '@/components/icons/IconClipboardEmpty.vue'
|
||||
import IconPhone from '@/components/icons/IconPhone.vue'
|
||||
import IconMail from '@/components/icons/IconMail.vue'
|
||||
import IconEditPen from '@/components/icons/IconEditPen.vue'
|
||||
import MeetingReportOverlay from '@/components/customers/MeetingReportOverlay.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -28,6 +30,7 @@ const crm = useCrmStore()
|
||||
const auth = useAuthStore()
|
||||
const id = computed(() => String(route.query.id ?? ''))
|
||||
const tab = computed(() => (String(route.query.tab ?? 'info') as CustomerTab))
|
||||
const showMeetingReport = computed(() => route.query.meetingReport === '1')
|
||||
const customer = ref<CrmCustomer | null>(null)
|
||||
const orders = ref<CrmOrderSummary[]>([])
|
||||
const meetings = ref<CrmMeeting[]>([])
|
||||
@@ -69,6 +72,25 @@ function setTab(next: CustomerTab) {
|
||||
router.replace({ path: '/customer', query: { id: id.value, tab: next } })
|
||||
}
|
||||
|
||||
function openMeetingReport() {
|
||||
router.push({
|
||||
path: '/customer',
|
||||
query: { id: id.value, tab: 'reports', meetingReport: '1' },
|
||||
})
|
||||
}
|
||||
|
||||
function closeMeetingReport() {
|
||||
router.replace({
|
||||
path: '/customer',
|
||||
query: { id: id.value, tab: 'reports' },
|
||||
})
|
||||
}
|
||||
|
||||
async function onMeetingReportSuccess() {
|
||||
closeMeetingReport()
|
||||
await load()
|
||||
}
|
||||
|
||||
function typeLabel(type: MeetingInteractionType) {
|
||||
if (type === 'in_person') return 'ملاقات حضوری'
|
||||
if (type === 'call') return 'تماس'
|
||||
@@ -117,6 +139,20 @@ function orderStatusTone(order: CrmOrderSummary): 'warning' | 'success' | 'info'
|
||||
if (variant === 'warning' || variant === 'danger') return 'warning'
|
||||
return order.statusTone || 'info'
|
||||
}
|
||||
|
||||
const recentOrders = computed(() => orders.value.slice(0, 3))
|
||||
|
||||
function customerStatusLabel(status: CustomerStatus) {
|
||||
if (status === 'active') return 'فعال'
|
||||
if (status === 'inactive') return 'غیرفعال'
|
||||
return 'جدید'
|
||||
}
|
||||
|
||||
function customerStatusClass(status: CustomerStatus) {
|
||||
if (status === 'active') return 'status-badge--active'
|
||||
if (status === 'inactive') return 'status-badge--inactive'
|
||||
return 'status-badge--new'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -133,22 +169,24 @@ function orderStatusTone(order: CrmOrderSummary): 'warning' | 'success' | 'info'
|
||||
</PageHeader>
|
||||
|
||||
<div class="page__scroll page__scroll--with-nav customer-scroll">
|
||||
<section v-if="tab !== 'reports'" class="profile-row">
|
||||
<div class="avatar" :style="{ background: customer.avatarColor }">
|
||||
<section v-if="tab !== 'reports'" class="profile-card">
|
||||
<div class="profile-card__avatar" :style="{ background: customer.avatarColor }">
|
||||
{{ customer.avatarLetter }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="name-row">
|
||||
<div class="profile-card__info">
|
||||
<div class="profile-card__name-row">
|
||||
<h2>{{ customer.name }}</h2>
|
||||
<span class="badge">فعال</span>
|
||||
<span class="status-badge" :class="customerStatusClass(customer.status)">
|
||||
{{ customerStatusLabel(customer.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<p dir="ltr">{{ customer.phone }}</p>
|
||||
<p class="profile-card__phone" dir="ltr">{{ customer.phone }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else class="profile-banner">
|
||||
<div class="profile-banner__top">
|
||||
<div class="avatar avatar--lg" :style="{ background: customer.avatarColor }">
|
||||
<div class="profile-card__avatar profile-card__avatar--lg" :style="{ background: customer.avatarColor }">
|
||||
{{ customer.avatarLetter }}
|
||||
</div>
|
||||
<div>
|
||||
@@ -156,7 +194,7 @@ function orderStatusTone(order: CrmOrderSummary): 'warning' | 'success' | 'info'
|
||||
<p>{{ customer.contactName }}</p>
|
||||
<p dir="ltr">{{ customer.phone }}</p>
|
||||
</div>
|
||||
<span class="badge badge--light">مشتری فعال</span>
|
||||
<span class="status-badge status-badge--active status-badge--on-dark">فعال</span>
|
||||
</div>
|
||||
<div class="profile-banner__stats">
|
||||
<div>
|
||||
@@ -174,54 +212,85 @@ function orderStatusTone(order: CrmOrderSummary): 'warning' | 'success' | 'info'
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tabs" role="tablist">
|
||||
<button
|
||||
v-for="item in tabs"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
role="tab"
|
||||
class="tab"
|
||||
:class="{ 'tab--active': tab === item.id }"
|
||||
:aria-selected="tab === item.id"
|
||||
@click="setTab(item.id)"
|
||||
>
|
||||
{{ item.label }}
|
||||
<span class="tab__text">{{ item.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template v-if="tab === 'info'">
|
||||
<section class="stats">
|
||||
<h3>اطلاعات خرید</h3>
|
||||
<div class="stats__grid">
|
||||
<div>
|
||||
<span>تعداد سفارش</span>
|
||||
<strong>{{ formatNumber(customer.orderCount) }}</strong>
|
||||
<section class="purchase-card">
|
||||
<h3 class="section-title">اطلاعات خرید</h3>
|
||||
<div class="purchase-stats">
|
||||
<div class="purchase-stats__item">
|
||||
<span class="purchase-stats__label">تعداد سفارش</span>
|
||||
<strong class="purchase-stats__value">{{ formatNumber(customer.orderCount) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>مجموع خرید</span>
|
||||
<strong>{{ formatNumber(customer.totalPurchaseToman) }}</strong>
|
||||
<em>تومان</em>
|
||||
<span class="purchase-stats__divider" aria-hidden="true" />
|
||||
<div class="purchase-stats__item">
|
||||
<span class="purchase-stats__label">مجموع خرید</span>
|
||||
<strong class="purchase-stats__value">{{ formatNumber(customer.totalPurchaseToman) }}</strong>
|
||||
<em class="purchase-stats__unit">تومان</em>
|
||||
</div>
|
||||
<div>
|
||||
<span>میانگین هر سفارش</span>
|
||||
<strong>{{ formatNumber(customer.averageOrderToman) }}</strong>
|
||||
<em>تومان</em>
|
||||
<span class="purchase-stats__divider" aria-hidden="true" />
|
||||
<div class="purchase-stats__item">
|
||||
<span class="purchase-stats__label">میانگین هر سفارش</span>
|
||||
<strong class="purchase-stats__value">{{ formatNumber(customer.averageOrderToman) }}</strong>
|
||||
<em class="purchase-stats__unit">تومان</em>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button type="button" class="agent-card" @click="router.push('/profile')">
|
||||
<div class="agent-card__avatar">{{ agentInitial }}</div>
|
||||
<div>
|
||||
<strong>{{ agentName }}</strong>
|
||||
<p>کد واسطه: {{ agentCode }}</p>
|
||||
</div>
|
||||
<IconChevronLeft :size="18" />
|
||||
</button>
|
||||
<section class="panel-card">
|
||||
<button type="button" class="agent-row" @click="router.push('/profile')">
|
||||
<div class="agent-row__avatar">{{ agentInitial }}</div>
|
||||
<div class="agent-row__info">
|
||||
<strong>{{ agentName }}</strong>
|
||||
<p>کد واسطه: {{ agentCode }}</p>
|
||||
</div>
|
||||
<IconChevronLeft :size="18" class="agent-row__chevron" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<h3>آخرین سفارشها</h3>
|
||||
<button type="button" class="link" @click="setTab('orders')">مشاهده همه</button>
|
||||
<div class="orders-block">
|
||||
<div class="orders-block__head">
|
||||
<h3 class="section-title">آخرین سفارشها</h3>
|
||||
<button type="button" class="link-btn" @click="setTab('orders')">مشاهده همه</button>
|
||||
</div>
|
||||
|
||||
<article
|
||||
v-for="order in recentOrders"
|
||||
:key="order.id"
|
||||
class="order-row"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="openOrder(order.id)"
|
||||
@keydown.enter="openOrder(order.id)"
|
||||
>
|
||||
<div class="order-row__title">
|
||||
<strong>{{ order.title }}</strong>
|
||||
<IconChevronLeft :size="14" class="order-row__chevron" aria-hidden="true" />
|
||||
</div>
|
||||
<span class="order-row__amount">{{ formatNumber(order.amountToman) }} تومان</span>
|
||||
<span class="status-pill" :class="`status-pill--${orderStatusTone(order)}`">
|
||||
{{ orderStatusLabel(order) }}
|
||||
</span>
|
||||
</article>
|
||||
<p v-if="!recentOrders.length" class="empty-inline">سفارشی ثبت نشده است.</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="tab === 'orders'">
|
||||
<section class="panel-card panel-card--list">
|
||||
<article
|
||||
v-for="order in orders"
|
||||
:key="order.id"
|
||||
@@ -231,74 +300,58 @@ function orderStatusTone(order: CrmOrderSummary): 'warning' | 'success' | 'info'
|
||||
@click="openOrder(order.id)"
|
||||
@keydown.enter="openOrder(order.id)"
|
||||
>
|
||||
<div>
|
||||
<div class="order-row__title">
|
||||
<strong>{{ order.title }}</strong>
|
||||
<p>{{ formatNumber(order.amountToman) }} تومان</p>
|
||||
<IconChevronLeft :size="14" class="order-row__chevron" aria-hidden="true" />
|
||||
</div>
|
||||
<span class="tone" :class="`tone--${orderStatusTone(order)}`">{{ orderStatusLabel(order) }}</span>
|
||||
<IconChevronLeft :size="16" class="order-row__chevron" />
|
||||
<span class="order-row__amount">{{ formatNumber(order.amountToman) }} تومان</span>
|
||||
<span class="status-pill" :class="`status-pill--${orderStatusTone(order)}`">
|
||||
{{ orderStatusLabel(order) }}
|
||||
</span>
|
||||
</article>
|
||||
<p v-if="!orders.length" class="empty-inline">سفارشی ثبت نشده است.</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="tab === 'orders'">
|
||||
<article
|
||||
v-for="order in orders"
|
||||
:key="order.id"
|
||||
class="order-row"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="openOrder(order.id)"
|
||||
@keydown.enter="openOrder(order.id)"
|
||||
>
|
||||
<div>
|
||||
<strong>{{ order.title }}</strong>
|
||||
<p>{{ order.date }} — {{ formatNumber(order.amountToman) }} تومان</p>
|
||||
</div>
|
||||
<span class="tone" :class="`tone--${orderStatusTone(order)}`">{{ orderStatusLabel(order) }}</span>
|
||||
<IconChevronLeft :size="16" class="order-row__chevron" />
|
||||
</article>
|
||||
<p v-if="!orders.length" class="empty-inline">سفارشی ثبت نشده است.</p>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<h3 class="block-title">تعاملات و پیگیریها</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="dashed-btn"
|
||||
@click="router.push({ path: '/meeting-report', query: { customerId: customer.id } })"
|
||||
>
|
||||
+ ثبت گزارش ملاقات جدید
|
||||
</button>
|
||||
<div class="meeting-filters">
|
||||
<section class="panel-card panel-card--reports">
|
||||
<h3 class="section-title">تعاملات و پیگیریها</h3>
|
||||
<button
|
||||
v-for="chip in meetingFilters"
|
||||
:key="chip.id"
|
||||
type="button"
|
||||
class="meeting-filter"
|
||||
:class="{ 'meeting-filter--active': meetingFilter === chip.id }"
|
||||
@click="meetingFilter = chip.id"
|
||||
class="dashed-btn"
|
||||
@click="openMeetingReport"
|
||||
>
|
||||
{{ chip.label }}
|
||||
+ ثبت گزارش ملاقات جدید
|
||||
</button>
|
||||
</div>
|
||||
<article v-for="item in filteredMeetings" :key="item.id" class="meeting-row">
|
||||
<div class="meeting-row__icon" :class="meetingIconClass(item.type)">
|
||||
<IconClipboardEmpty v-if="item.type === 'in_person'" :size="16" />
|
||||
<IconPhone v-else-if="item.type === 'call'" :size="16" />
|
||||
<IconMail v-else-if="item.type === 'message'" :size="16" />
|
||||
<IconEditPen v-else :size="16" />
|
||||
<div class="meeting-filters">
|
||||
<button
|
||||
v-for="chip in meetingFilters"
|
||||
:key="chip.id"
|
||||
type="button"
|
||||
class="meeting-filter"
|
||||
:class="{ 'meeting-filter--active': meetingFilter === chip.id }"
|
||||
@click="meetingFilter = chip.id"
|
||||
>
|
||||
{{ chip.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="meeting-row__body">
|
||||
<strong>{{ typeLabel(item.type) }}</strong>
|
||||
<p>{{ item.description || item.title }}</p>
|
||||
</div>
|
||||
<span>{{ item.date }}</span>
|
||||
</article>
|
||||
<p v-if="!filteredMeetings.length" class="empty-inline">
|
||||
گزارشی برای این فیلتر ثبت نشده است.
|
||||
</p>
|
||||
<article v-for="item in filteredMeetings" :key="item.id" class="meeting-row">
|
||||
<div class="meeting-row__icon" :class="meetingIconClass(item.type)">
|
||||
<IconClipboardEmpty v-if="item.type === 'in_person'" :size="16" />
|
||||
<IconPhone v-else-if="item.type === 'call'" :size="16" />
|
||||
<IconMail v-else-if="item.type === 'message'" :size="16" />
|
||||
<IconEditPen v-else :size="16" />
|
||||
</div>
|
||||
<div class="meeting-row__body">
|
||||
<strong>{{ typeLabel(item.type) }}</strong>
|
||||
<p>{{ item.description || item.title }}</p>
|
||||
</div>
|
||||
<span class="meeting-row__date">{{ item.date }}</span>
|
||||
</article>
|
||||
<p v-if="!filteredMeetings.length" class="empty-inline">
|
||||
گزارشی برای این فیلتر ثبت نشده است.
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -317,6 +370,13 @@ function orderStatusTone(order: CrmOrderSummary): 'warning' | 'success' | 'info'
|
||||
<div v-else class="page empty">
|
||||
<p>{{ error || 'مشتری یافت نشد.' }}</p>
|
||||
</div>
|
||||
|
||||
<MeetingReportOverlay
|
||||
:open="showMeetingReport"
|
||||
:customer-id="id"
|
||||
@close="closeMeetingReport"
|
||||
@success="onMeetingReportSuccess"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -325,62 +385,100 @@ function orderStatusTone(order: CrmOrderSummary): 'warning' | 'success' | 'info'
|
||||
}
|
||||
|
||||
.customer-scroll {
|
||||
padding: 8px var(--page-padding) 100px;
|
||||
padding: 0 var(--page-padding) 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.profile-row {
|
||||
.profile-card,
|
||||
.purchase-card,
|
||||
.panel-card {
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 12px 0 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
.profile-card__avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 999px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 1.4rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #111;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar--lg {
|
||||
.profile-card__avatar--lg {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.name-row {
|
||||
.profile-card__info {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.profile-card__name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.name-row h2,
|
||||
.profile-card__name-row h2,
|
||||
.profile-banner h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.profile-row p,
|
||||
.profile-banner p {
|
||||
margin: 2px 0 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
font-size: 0.75rem;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.badge--light {
|
||||
.profile-card__phone,
|
||||
.profile-banner p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-badge--active {
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.status-badge--inactive {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.status-badge--new {
|
||||
background: #ede9fe;
|
||||
color: #6d28d9;
|
||||
}
|
||||
|
||||
.status-badge--on-dark {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
}
|
||||
@@ -390,6 +488,7 @@ function orderStatusTone(order: CrmOrderSummary): 'warning' | 'success' | 'info'
|
||||
color: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 6px 16px rgba(16, 24, 40, 0.05);
|
||||
}
|
||||
|
||||
.profile-banner__top {
|
||||
@@ -422,169 +521,269 @@ function orderStatusTone(order: CrmOrderSummary): 'warning' | 'success' | 'info'
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tab {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 10px 2px;
|
||||
color: var(--color-text-muted);
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
min-width: 0;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
border: 0;
|
||||
background: none;
|
||||
padding: 10px 0 0;
|
||||
color: #9ca3af;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab__text {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding-bottom: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab--active {
|
||||
color: var(--color-text);
|
||||
font-weight: 700;
|
||||
box-shadow: inset 0 -3px 0 var(--color-text);
|
||||
}
|
||||
|
||||
.stats,
|
||||
.agent-card,
|
||||
.order-row,
|
||||
.meeting-row {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 14px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
.tab--active .tab__text::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
bottom: -1px;
|
||||
height: 3px;
|
||||
background: var(--color-primary);
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
|
||||
.stats h3,
|
||||
.section-head h3,
|
||||
.block-title {
|
||||
margin: 0 0 10px;
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.stats__grid {
|
||||
.purchase-card {
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.purchase-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
grid-template-columns: 1fr 1px 1fr 1px 1fr;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.stats__grid span,
|
||||
.stats__grid em {
|
||||
display: block;
|
||||
font-size: 0.7rem;
|
||||
.purchase-stats__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
padding: 0 6px;
|
||||
text-align: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.purchase-stats__divider {
|
||||
width: 1px;
|
||||
background: var(--color-border);
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.purchase-stats__label {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.purchase-stats__value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.purchase-stats__unit {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-muted);
|
||||
font-style: normal;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.stats__grid strong {
|
||||
display: block;
|
||||
margin: 4px 0;
|
||||
font-size: 0.9rem;
|
||||
.panel-card {
|
||||
padding: 0 0 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.agent-card {
|
||||
width: 100%;
|
||||
border: none;
|
||||
display: grid;
|
||||
grid-template-columns: 44px 1fr 20px;
|
||||
gap: 10px;
|
||||
.panel-card--list,
|
||||
.panel-card--reports {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.agent-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 16px 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: transparent;
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.agent-card__avatar {
|
||||
.agent-row__avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-primary-soft);
|
||||
background: #e8ecea;
|
||||
color: var(--color-primary);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.agent-card p {
|
||||
margin: 2px 0 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-info);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.order-row,
|
||||
.meeting-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.order-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.order-row__chevron {
|
||||
color: var(--color-text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.meeting-row__body {
|
||||
.agent-row__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.order-row p,
|
||||
.meeting-row p {
|
||||
margin: 2px 0 0;
|
||||
.agent-row__info strong {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.agent-row__info p {
|
||||
margin: 4px 0 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.agent-row__chevron {
|
||||
color: #9ca3af;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.orders-block {
|
||||
padding: 14px 0 8px;
|
||||
}
|
||||
|
||||
.orders-block__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #6b7280;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.order-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.panel-card--list .order-row:last-of-type,
|
||||
.orders-block .order-row:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.order-row__title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.order-row__title strong {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.order-row__chevron {
|
||||
color: #9ca3af;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.order-row__amount {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 26px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-pill--warning {
|
||||
background: #fff4e5;
|
||||
color: #c27803;
|
||||
}
|
||||
|
||||
.status-pill--success {
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.status-pill--info {
|
||||
background: var(--color-info-light);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.empty-inline {
|
||||
margin: 8px 0 0;
|
||||
margin: 12px 0 4px;
|
||||
text-align: center;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.tone {
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
font-size: 0.72rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tone--warning {
|
||||
background: var(--color-warning-light);
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.tone--success {
|
||||
background: var(--color-success-light);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.tone--info {
|
||||
background: var(--color-info-light);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-info, #2563eb);
|
||||
color: #3b82f6;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
@@ -596,19 +795,20 @@ function orderStatusTone(order: CrmOrderSummary): 'warning' | 'success' | 'info'
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 8px;
|
||||
margin: 12px 0 8px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.meeting-filter {
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
border-radius: 999px;
|
||||
background: var(--color-bg);
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.meeting-filter--active {
|
||||
@@ -627,7 +827,20 @@ function orderStatusTone(order: CrmOrderSummary): 'warning' | 'success' | 'info'
|
||||
padding: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 12px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.meeting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.meeting-row:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.meeting-row__icon {
|
||||
@@ -654,11 +867,34 @@ function orderStatusTone(order: CrmOrderSummary): 'warning' | 'success' | 'info'
|
||||
color: #6d28d9;
|
||||
}
|
||||
|
||||
.meeting-row__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.meeting-row__body strong {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.meeting-row__body p {
|
||||
margin: 4px 0 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.meeting-row__date {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cta-bar {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding: 12px var(--page-padding) calc(12px + env(safe-area-inset-bottom, 0));
|
||||
background: linear-gradient(transparent, var(--color-bg) 30%);
|
||||
background: linear-gradient(180deg, rgba(238, 241, 240, 0) 0%, var(--color-bg) 32%);
|
||||
}
|
||||
|
||||
.cta {
|
||||
@@ -668,12 +904,14 @@ function orderStatusTone(order: CrmOrderSummary): 'warning' | 'success' | 'info'
|
||||
border-radius: 14px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.empty {
|
||||
|
||||
@@ -11,6 +11,7 @@ import IconFilter from '@/components/icons/IconFilter.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconPlus from '@/components/icons/IconPlus.vue'
|
||||
import IconProfile from '@/components/icons/IconProfile.vue'
|
||||
import CustomerCreateOverlay from '@/components/customers/CustomerCreateOverlay.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -22,6 +23,7 @@ const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const filtered = computed(() => customers.value)
|
||||
const showCreate = computed(() => route.query.create === '1')
|
||||
|
||||
const activeCount = computed(() => customers.value.filter((c) => c.status === 'active').length)
|
||||
const totalPurchase = computed(() =>
|
||||
@@ -81,8 +83,29 @@ function statusLabel(s: CustomerStatus) {
|
||||
return 'جدید'
|
||||
}
|
||||
|
||||
function customerListQuery(extra: Record<string, string> = {}) {
|
||||
return {
|
||||
...(status.value !== 'all' ? { status: status.value } : {}),
|
||||
...(q.value ? { q: q.value } : {}),
|
||||
...extra,
|
||||
}
|
||||
}
|
||||
|
||||
function onRegisterCustomer() {
|
||||
router.push('/customers/new')
|
||||
router.push({ path: '/customers', query: { ...customerListQuery(), create: '1' } })
|
||||
}
|
||||
|
||||
function closeCreate() {
|
||||
router.replace({ path: '/customers', query: customerListQuery() })
|
||||
}
|
||||
|
||||
async function onCustomerCreated(id?: string) {
|
||||
if (id) {
|
||||
await router.replace({ path: '/customer', query: { id } })
|
||||
return
|
||||
}
|
||||
closeCreate()
|
||||
await load()
|
||||
}
|
||||
|
||||
function cycleStatusFilter() {
|
||||
@@ -196,6 +219,12 @@ function cycleStatusFilter() {
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<CustomerCreateOverlay
|
||||
:open="showCreate"
|
||||
@close="closeCreate"
|
||||
@success="onCustomerCreated"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,430 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useCrmStore } from '@/stores/crm'
|
||||
import type { CrmCustomer, MeetingInteractionType } from '@/types/crm'
|
||||
import PageHeader from '@/components/layout/PageHeader.vue'
|
||||
import PageSkeleton from '@/components/ui/PageSkeleton.vue'
|
||||
import IconCheckCircleOutline from '@/components/icons/IconCheckCircleOutline.vue'
|
||||
import IconClipboardEmpty from '@/components/icons/IconClipboardEmpty.vue'
|
||||
import IconPhone from '@/components/icons/IconPhone.vue'
|
||||
import IconMail from '@/components/icons/IconMail.vue'
|
||||
import IconEditPen from '@/components/icons/IconEditPen.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const crm = useCrmStore()
|
||||
const customerId = computed(() => String(route.query.customerId ?? ''))
|
||||
const customer = ref<CrmCustomer | null>(null)
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const interactionType = ref<MeetingInteractionType>('in_person')
|
||||
const date = ref('')
|
||||
const time = ref('')
|
||||
const subject = ref('')
|
||||
const content = ref('')
|
||||
const reminderOn = ref(true)
|
||||
const reminderTitle = ref('')
|
||||
const reminderDate = ref('')
|
||||
const reminderTime = ref('')
|
||||
const reminderNote = ref('')
|
||||
|
||||
const types: Array<{ id: MeetingInteractionType; label: string; icon: 'clipboard' | 'phone' | 'mail' | 'pen' }> = [
|
||||
{ id: 'in_person', label: 'ملاقات حضوری', icon: 'clipboard' },
|
||||
{ id: 'call', label: 'تماس تلفنی', icon: 'phone' },
|
||||
{ id: 'message', label: 'پیام', icon: 'mail' },
|
||||
{ id: 'note', label: 'یادداشت', icon: 'pen' },
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
if (!customerId.value) {
|
||||
loading.value = false
|
||||
error.value = 'مشتری مشخص نشده است.'
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await crm.loadCustomerDetail(customerId.value)
|
||||
customer.value = data.customer
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'بارگذاری مشتری ناموفق بود.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
async function submit() {
|
||||
if (!customerId.value || !subject.value.trim() || !date.value.trim()) {
|
||||
error.value = 'موضوع و تاریخ الزامی است.'
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await crm.submitMeeting({
|
||||
customerId: customerId.value,
|
||||
type: interactionType.value,
|
||||
title: subject.value.trim(),
|
||||
subject: subject.value.trim(),
|
||||
content: content.value.trim() || undefined,
|
||||
description: content.value.trim() || undefined,
|
||||
date: date.value.trim(),
|
||||
time: time.value.trim() || undefined,
|
||||
createReminder: reminderOn.value,
|
||||
reminderTitle: reminderOn.value
|
||||
? reminderTitle.value.trim() || subject.value.trim()
|
||||
: undefined,
|
||||
reminderDate: reminderOn.value
|
||||
? reminderDate.value.trim() || date.value.trim()
|
||||
: undefined,
|
||||
reminderTime: reminderOn.value
|
||||
? reminderTime.value.trim() || time.value.trim() || '10:00'
|
||||
: undefined,
|
||||
reminderNote: reminderOn.value
|
||||
? reminderNote.value.trim() || undefined
|
||||
: undefined,
|
||||
})
|
||||
router.push({
|
||||
path: '/customer',
|
||||
query: { id: customerId.value, tab: 'reports' },
|
||||
})
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'ثبت گزارش ناموفق بود.'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page meeting-page">
|
||||
<PageHeader title="ثبت گزارش ملاقات" show-back centered @back="router.back()">
|
||||
<template #trailing>
|
||||
<button
|
||||
type="button"
|
||||
class="submit-icon-btn"
|
||||
aria-label="ثبت گزارش"
|
||||
:disabled="saving"
|
||||
@click="submit"
|
||||
>
|
||||
<IconCheckCircleOutline :size="22" />
|
||||
</button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<div class="page__scroll meeting-scroll">
|
||||
<PageSkeleton v-if="loading" variant="form" />
|
||||
<p v-else-if="error" class="form-error">{{ error }}</p>
|
||||
<section v-if="customer" class="customer-card">
|
||||
<div class="avatar" :style="{ background: customer.avatarColor }">
|
||||
{{ customer.avatarLetter }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ customer.name }}</strong>
|
||||
<p>{{ customer.contactName }}</p>
|
||||
<p dir="ltr">{{ customer.phone }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>نوع تعامل</h3>
|
||||
<div class="type-grid">
|
||||
<button
|
||||
v-for="item in types"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="type-btn"
|
||||
:class="{ 'type-btn--active': interactionType === item.id }"
|
||||
@click="interactionType = item.id"
|
||||
>
|
||||
<IconClipboardEmpty v-if="item.icon === 'clipboard'" :size="22" />
|
||||
<IconPhone v-else-if="item.icon === 'phone'" :size="22" />
|
||||
<IconMail v-else-if="item.icon === 'mail'" :size="22" />
|
||||
<IconEditPen v-else :size="22" />
|
||||
<span>{{ item.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>تاریخ و زمان</h3>
|
||||
<div class="two-cols">
|
||||
<label class="field">
|
||||
<span>تاریخ</span>
|
||||
<input v-model="date" type="text" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>ساعت</span>
|
||||
<input v-model="time" type="text" />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<label class="field">
|
||||
<span>موضوع گزارش</span>
|
||||
<input v-model="subject" type="text" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>محتوای گزارش</span>
|
||||
<textarea v-model="content" rows="5" maxlength="1000" />
|
||||
<em>{{ content.length.toLocaleString('fa-IR') }}/۱۰۰۰</em>
|
||||
</label>
|
||||
|
||||
<section>
|
||||
<h3>تصاویر و فایلها</h3>
|
||||
<div class="files">
|
||||
<div class="file-thumb" />
|
||||
<div class="file-thumb" />
|
||||
<button type="button" class="file-add" aria-label="افزودن فایل">+</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="reminder-box">
|
||||
<div class="reminder-box__head">
|
||||
<h3>یادآور</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle"
|
||||
:class="{ 'toggle--on': reminderOn }"
|
||||
:aria-pressed="reminderOn"
|
||||
@click="reminderOn = !reminderOn"
|
||||
>
|
||||
<span />
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="reminderOn">
|
||||
<label class="field">
|
||||
<span>عنوان یادآور</span>
|
||||
<input v-model="reminderTitle" type="text" />
|
||||
</label>
|
||||
<div class="two-cols">
|
||||
<label class="field">
|
||||
<span>تاریخ یادآور</span>
|
||||
<input v-model="reminderDate" type="text" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>ساعت یادآور</span>
|
||||
<input v-model="reminderTime" type="text" />
|
||||
</label>
|
||||
</div>
|
||||
<label class="field">
|
||||
<span>یادداشت یادآور (اختیاری)</span>
|
||||
<input v-model="reminderNote" type="text" />
|
||||
</label>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<button type="button" class="submit" :disabled="saving" @click="submit">
|
||||
{{ saving ? 'در حال ثبت...' : 'ثبت گزارش و یادآور' }}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.meeting-page {
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.meeting-scroll {
|
||||
padding: 8px var(--page-padding) 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.customer-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 999px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.customer-card p {
|
||||
margin: 2px 0 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.type-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.submit-icon-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.submit-icon-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
border: 1.5px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
border-radius: 12px;
|
||||
min-height: 72px;
|
||||
padding: 8px 6px;
|
||||
font-size: 0.72rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.type-btn--active {
|
||||
border-color: var(--color-primary);
|
||||
border-width: 2px;
|
||||
color: var(--color-primary);
|
||||
font-weight: 700;
|
||||
background: var(--color-primary-soft);
|
||||
}
|
||||
|
||||
.two-cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field textarea {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
background: #f5f5f5;
|
||||
padding: 12px;
|
||||
font: inherit;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.field em {
|
||||
font-style: normal;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.75rem;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.files {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-thumb,
|
||||
.file-add {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.file-thumb {
|
||||
background: linear-gradient(145deg, #4b5563, #1f2937);
|
||||
}
|
||||
|
||||
.file-add {
|
||||
border: 1.5px dashed var(--color-border);
|
||||
background: transparent;
|
||||
font-size: 1.4rem;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reminder-box {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.reminder-box__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
width: 46px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: #d1d5db;
|
||||
padding: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle span {
|
||||
display: block;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
.toggle--on {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.toggle--on span {
|
||||
transform: translateX(-18px);
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding: 12px var(--page-padding) calc(12px + env(safe-area-inset-bottom, 0));
|
||||
background: var(--color-surface);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.submit {
|
||||
width: 100%;
|
||||
min-height: 52px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
+91
-30
@@ -468,10 +468,6 @@ async function toggleFavorite(productId: string) {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.home-scroll > .section:last-child {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.hero-banner {
|
||||
--hero-image: #8a7a65;
|
||||
position: relative;
|
||||
@@ -515,6 +511,11 @@ async function toggleFavorite(productId: string) {
|
||||
.hero-banner__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
text-align: start;
|
||||
animation: hero-copy 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
@@ -592,6 +593,7 @@ async function toggleFavorite(productId: string) {
|
||||
line-height: 1.7;
|
||||
max-width: 16rem;
|
||||
font-weight: 700;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.hero-banner__title span {
|
||||
@@ -644,7 +646,7 @@ async function toggleFavorite(productId: string) {
|
||||
|
||||
.quick-card {
|
||||
border: none;
|
||||
background: transparent;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px 6px;
|
||||
display: flex;
|
||||
@@ -652,7 +654,7 @@ async function toggleFavorite(productId: string) {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
box-shadow: none;
|
||||
box-shadow: 0 2px 10px rgba(16, 24, 40, 0.06);
|
||||
}
|
||||
|
||||
.quick-card__icon {
|
||||
@@ -661,8 +663,9 @@ async function toggleFavorite(productId: string) {
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--color-surface);
|
||||
box-shadow: 0 2px 12px rgba(16, 24, 40, 0.06);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.quick-card__image {
|
||||
@@ -683,8 +686,8 @@ async function toggleFavorite(productId: string) {
|
||||
background: var(--color-surface);
|
||||
border-radius: 18px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 12px rgba(16, 24, 40, 0.06);
|
||||
border: 1px solid rgba(230, 232, 230, 0.9);
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.level-card__top {
|
||||
@@ -819,7 +822,8 @@ async function toggleFavorite(productId: string) {
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(16, 24, 40, 0.05);
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--color-border);
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
@@ -912,7 +916,11 @@ async function toggleFavorite(productId: string) {
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.home-page {
|
||||
padding-top: 8px;
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
padding: 8px var(--page-padding) 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.home-desk-head {
|
||||
@@ -938,38 +946,87 @@ async function toggleFavorite(productId: string) {
|
||||
}
|
||||
|
||||
.home-scroll {
|
||||
gap: 22px;
|
||||
padding: 8px 0 24px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-areas:
|
||||
'hero hero level'
|
||||
'quick quick quick'
|
||||
'products products products';
|
||||
gap: 18px 20px;
|
||||
padding: 8px 0 28px;
|
||||
align-items: stretch;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero-banner {
|
||||
min-height: 220px;
|
||||
grid-area: hero;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
aspect-ratio: 2.1 / 1;
|
||||
align-self: start;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.hero-slide {
|
||||
padding: 28px 32px;
|
||||
}
|
||||
|
||||
.hero-banner__title {
|
||||
font-size: 1.35rem;
|
||||
max-width: 22rem;
|
||||
.level-card {
|
||||
grid-area: level;
|
||||
padding: 20px 22px;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quick-grid {
|
||||
grid-area: quick;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.home-scroll > .section {
|
||||
grid-area: products;
|
||||
}
|
||||
|
||||
.hero-slide {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
padding-inline-start: 36px;
|
||||
padding-inline-end: 28px;
|
||||
padding-block: 24px;
|
||||
}
|
||||
|
||||
.hero-slide__media {
|
||||
background-size: cover;
|
||||
background-position: left center;
|
||||
background-color: #8a7a65;
|
||||
}
|
||||
|
||||
.hero-banner__content {
|
||||
max-width: min(52%, 22rem);
|
||||
}
|
||||
|
||||
.hero-banner__title {
|
||||
font-size: 1.15rem;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.hero-next-enter-from,
|
||||
.hero-next-leave-to,
|
||||
.hero-prev-enter-from,
|
||||
.hero-prev-leave-to {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.quick-card {
|
||||
min-height: 96px;
|
||||
padding: 18px 14px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.level-card {
|
||||
padding: 20px 22px;
|
||||
box-shadow: 0 3px 12px rgba(16, 24, 40, 0.07);
|
||||
}
|
||||
|
||||
.product-row {
|
||||
@@ -993,12 +1050,16 @@ async function toggleFavorite(productId: string) {
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.hero-banner {
|
||||
aspect-ratio: 2.2 / 1;
|
||||
}
|
||||
|
||||
.hero-banner__title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.product-row {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.hero-banner {
|
||||
min-height: 240px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { formatNumber } from '@/utils/format'
|
||||
import { colorImageAt, colorSwatchStyle } from '@/utils/colorSwatch'
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import IconCart from '@/components/icons/IconCart.vue'
|
||||
import IconCatalog from '@/components/icons/IconCatalog.vue'
|
||||
import quickCatalogRequestIcon from '@/assets/images/quick-catalog-request.webp'
|
||||
import IconHeart from '@/components/icons/IconHeart.vue'
|
||||
import IconShare from '@/components/icons/IconShare.vue'
|
||||
import IconShoppingBag from '@/components/icons/IconShoppingBag.vue'
|
||||
@@ -379,7 +379,7 @@ function shareProduct() {
|
||||
<p class="min-note">حداقل سفارش: {{ formatNumber(minRolls) }} طاقه</p>
|
||||
</div>
|
||||
<button type="button" class="btn-secondary" @click="router.push({ path: '/catalog-request', query: { productId: product.id } })">
|
||||
<IconCatalog :size="18" />
|
||||
<img class="btn-secondary__icon" :src="quickCatalogRequestIcon" alt="" aria-hidden="true" />
|
||||
درخواست کالیته
|
||||
</button>
|
||||
</footer>
|
||||
@@ -400,7 +400,7 @@ function shareProduct() {
|
||||
--pd-card-border: 1px solid #eeeef1;
|
||||
--pd-card-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 6px 16px rgba(16, 24, 40, 0.05);
|
||||
--pd-card-radius: 16px;
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
@@ -446,7 +446,7 @@ function shareProduct() {
|
||||
inset-inline-start: 12px;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
border-radius: 999px;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
@@ -457,7 +457,7 @@ function shareProduct() {
|
||||
margin: 12px var(--page-padding) 0;
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
border: var(--pd-card-border);
|
||||
border-radius: var(--pd-card-radius);
|
||||
box-shadow: var(--pd-card-shadow);
|
||||
@@ -470,7 +470,7 @@ function shareProduct() {
|
||||
height: 52px;
|
||||
border-radius: 14px;
|
||||
border: var(--pd-card-border);
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--pd-card-shadow);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
@@ -499,7 +499,7 @@ function shareProduct() {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.title-row {
|
||||
@@ -541,7 +541,7 @@ function shareProduct() {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
padding: 14px 8px;
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
border: var(--pd-card-border);
|
||||
border-radius: var(--pd-card-radius);
|
||||
box-shadow: var(--pd-card-shadow);
|
||||
@@ -631,7 +631,7 @@ function shareProduct() {
|
||||
|
||||
.pd-section--boxed {
|
||||
padding: 12px 14px;
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
border: var(--pd-card-border);
|
||||
border-radius: var(--pd-card-radius);
|
||||
box-shadow: var(--pd-card-shadow);
|
||||
@@ -704,8 +704,8 @@ button.tag {
|
||||
min-height: 32px;
|
||||
padding: 0 16px;
|
||||
border: var(--pd-card-border);
|
||||
border-radius: var(--pd-card-radius);
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
background: var(--color-surface);
|
||||
color: #6b7280;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 400;
|
||||
@@ -726,7 +726,7 @@ button.tag--link {
|
||||
}
|
||||
|
||||
button.tag--link:hover {
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.roll-field {
|
||||
@@ -734,11 +734,11 @@ button.tag--link:hover {
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
border: 1px solid #eeeef1;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 6px 16px rgba(16, 24, 40, 0.05);
|
||||
padding: 14px 16px;
|
||||
border: var(--pd-card-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--pd-card-shadow);
|
||||
padding: 0 16px;
|
||||
color: #6b7280;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -780,6 +780,14 @@ button.tag--link:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.btn-secondary__icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
border: none;
|
||||
background: var(--color-accent-navy);
|
||||
|
||||
@@ -9,8 +9,19 @@ import SearchField from '@/components/ui/SearchField.vue'
|
||||
import IconFilter from '@/components/icons/IconFilter.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconHeart from '@/components/icons/IconHeart.vue'
|
||||
import IconCatalog from '@/components/icons/IconCatalog.vue'
|
||||
import IconDiamond from '@/components/icons/IconDiamond.vue'
|
||||
import { useFavoritesStore } from '@/stores/favorites'
|
||||
import { colorSwatchStyle } from '@/utils/colorSwatch'
|
||||
import { categoryIconOf, usageCategoryIconKey } from '@/utils/categoryIcons'
|
||||
|
||||
const CATEGORY_TAGLINES: Record<string, string> = {
|
||||
suit: 'شیک، بادوام و رسمی',
|
||||
trousers: 'مناسب شلوار و مصارف روزانه',
|
||||
shirt: 'سبک و مناسب پیراهن',
|
||||
}
|
||||
|
||||
const GENERIC_CATEGORY_CTA = 'مشاهده محصولات'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -43,15 +54,81 @@ const title = computed(() => {
|
||||
return 'همه محصولات'
|
||||
})
|
||||
|
||||
const bannerSubtitle = computed(() => {
|
||||
if (catalogId.value) return 'انتخاب محصول از کالیته'
|
||||
if (categoryId.value === 'suit') return 'شیک، بادوام و رسمی'
|
||||
if (categoryId.value === 'trousers') return 'مناسب شلوار و مصارف روزانه'
|
||||
if (categoryId.value === 'shirt') return 'سبک و مناسب پیراهن'
|
||||
if (season.value) return 'فیلتر شده بر اساس فصل'
|
||||
return 'مشاهده و ثبت سفارش'
|
||||
function splitCategoryTitle(fullTitle: string) {
|
||||
const trimmed = fullTitle.trim()
|
||||
const prefix = 'پارچههای '
|
||||
if (trimmed.startsWith(prefix)) {
|
||||
return { lead: 'پارچههای', headline: trimmed.slice(prefix.length) }
|
||||
}
|
||||
return { lead: undefined, headline: trimmed }
|
||||
}
|
||||
|
||||
function seasonIconKey(item: SeasonCategory) {
|
||||
return item.icon || (item.id === 'summer' ? 'bolt' : item.id === 'winter' ? 'feather' : 'diamond')
|
||||
}
|
||||
|
||||
const bannerCopy = computed(() => {
|
||||
if (categoryId.value) {
|
||||
const cat = crm.usageCategories.find((c) => c.id === categoryId.value)
|
||||
const parts = splitCategoryTitle(cat?.title ?? title.value)
|
||||
const tagline =
|
||||
CATEGORY_TAGLINES[categoryId.value] ??
|
||||
(cat?.subtitle && cat.subtitle !== GENERIC_CATEGORY_CTA ? cat.subtitle : undefined)
|
||||
return { ...parts, tagline }
|
||||
}
|
||||
if (season.value) {
|
||||
const item = seasons.value.find((s) => s.id === season.value)
|
||||
return {
|
||||
lead: undefined,
|
||||
headline: item?.title ?? title.value,
|
||||
tagline: item?.description || 'فیلتر شده بر اساس فصل',
|
||||
}
|
||||
}
|
||||
if (catalogId.value) {
|
||||
return {
|
||||
lead: 'کالیته',
|
||||
headline: catalogTitle.value || title.value,
|
||||
tagline: 'انتخاب محصول از کالیته',
|
||||
}
|
||||
}
|
||||
return {
|
||||
lead: undefined,
|
||||
headline: title.value,
|
||||
tagline: q.value ? 'نتایج جستجو' : 'مشاهده و ثبت سفارش',
|
||||
}
|
||||
})
|
||||
|
||||
const bannerIcon = computed(() => {
|
||||
if (categoryId.value) {
|
||||
const cat = crm.usageCategories.find((c) => c.id === categoryId.value)
|
||||
if (cat) return categoryIconOf(usageCategoryIconKey(cat), IconCatalog)
|
||||
}
|
||||
if (season.value) {
|
||||
const item = seasons.value.find((s) => s.id === season.value)
|
||||
if (item) return categoryIconOf(seasonIconKey(item), IconDiamond)
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const bannerImageUrl = computed(() => {
|
||||
if (categoryId.value) {
|
||||
const cat = crm.usageCategories.find((c) => c.id === categoryId.value)
|
||||
if (cat?.imageUrl) return cat.imageUrl
|
||||
}
|
||||
if (season.value) {
|
||||
const item = seasons.value.find((s) => s.id === season.value)
|
||||
if (item?.imageUrl) return item.imageUrl
|
||||
}
|
||||
if (catalogId.value) {
|
||||
return products.value.find((item) => item.imageUrl)?.imageUrl
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const bannerStyle = computed(() =>
|
||||
bannerImageUrl.value ? { backgroundImage: `url(${bannerImageUrl.value})` } : undefined,
|
||||
)
|
||||
|
||||
const showSearch = computed(() => !categoryId.value && !catalogId.value)
|
||||
|
||||
const sortedProducts = computed(() => {
|
||||
@@ -75,6 +152,10 @@ function cycleSort() {
|
||||
sortMode.value = sortMode.value === 'newest' ? 'name' : 'newest'
|
||||
}
|
||||
|
||||
const sortAriaLabel = computed(() =>
|
||||
sortMode.value === 'newest' ? 'مرتبسازی: جدیدترین' : 'مرتبسازی: نام',
|
||||
)
|
||||
|
||||
function applySearch(value: string) {
|
||||
searchDraft.value = value
|
||||
router.replace({
|
||||
@@ -168,11 +249,18 @@ function toggleFavorite(productId: string) {
|
||||
<p v-if="error">{{ error }}</p>
|
||||
<PageSkeleton v-else-if="loading" variant="grid" />
|
||||
<template v-else>
|
||||
<section class="cat-banner">
|
||||
<div class="cat-banner__icon" aria-hidden="true" />
|
||||
<div>
|
||||
<h2>{{ title }}</h2>
|
||||
<p>{{ bannerSubtitle }}</p>
|
||||
<section
|
||||
class="cat-banner"
|
||||
:class="{ 'cat-banner--photo': !!bannerImageUrl }"
|
||||
:style="bannerStyle"
|
||||
>
|
||||
<div v-if="bannerIcon" class="cat-banner__icon-ring" aria-hidden="true">
|
||||
<component :is="bannerIcon" :size="26" color="#ffffff" />
|
||||
</div>
|
||||
<div class="cat-banner__copy">
|
||||
<p v-if="bannerCopy.lead" class="cat-banner__lead">{{ bannerCopy.lead }}</p>
|
||||
<h2 class="cat-banner__title">{{ bannerCopy.headline }}</h2>
|
||||
<p v-if="bannerCopy.tagline" class="cat-banner__tagline">{{ bannerCopy.tagline }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -199,8 +287,34 @@ function toggleFavorite(productId: string) {
|
||||
|
||||
<div class="list-meta">
|
||||
<span>تعداد محصولات: {{ sortedProducts.length.toLocaleString('fa-IR') }}</span>
|
||||
<button type="button" class="list-meta__sort" @click="cycleSort">
|
||||
مرتبسازی: {{ sortMode === 'newest' ? 'جدیدترین' : 'نام' }}
|
||||
<button
|
||||
type="button"
|
||||
class="list-meta__sort"
|
||||
:aria-label="sortAriaLabel"
|
||||
@click="cycleSort"
|
||||
>
|
||||
<span class="list-meta__sort-icon" aria-hidden="true">
|
||||
<svg width="15" height="12" viewBox="0 0 15 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.5 2.5V9.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||
<path
|
||||
d="M2.5 7.5L4.5 9.5L6.5 7.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M10.5 9.5V2.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" />
|
||||
<path
|
||||
d="M8.5 4.5L10.5 2.5L12.5 4.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="list-meta__sort-badge">۱</span>
|
||||
<span class="list-meta__sort-label">مرتبسازی</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -288,35 +402,86 @@ function toggleFavorite(productId: string) {
|
||||
}
|
||||
|
||||
.cat-banner {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 96px;
|
||||
padding: 16px;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-height: 112px;
|
||||
padding: 18px 20px;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(100deg, rgba(var(--color-primary-rgb), 0.75), rgba(var(--color-primary-rgb), 0.35)),
|
||||
#2f3430;
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
background-repeat: no-repeat;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.cat-banner__icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
flex-shrink: 0;
|
||||
.cat-banner--photo::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, 0.58) 0%,
|
||||
rgba(0, 0, 0, 0.28) 42%,
|
||||
rgba(0, 0, 0, 0.08) 68%,
|
||||
transparent 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cat-banner h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 1rem;
|
||||
.cat-banner__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.cat-banner p {
|
||||
.cat-banner__lead {
|
||||
margin: 0 0 2px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
opacity: 0.95;
|
||||
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.cat-banner__title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.cat-banner__tagline {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.9;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
opacity: 0.92;
|
||||
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.cat-banner__icon-ring {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 999px;
|
||||
border: 1.5px solid rgba(255, 255, 255, 0.88);
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.chips {
|
||||
@@ -327,9 +492,9 @@ function toggleFavorite(productId: string) {
|
||||
|
||||
.chip {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 8px 14px;
|
||||
background: #ecefee;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
@@ -350,15 +515,27 @@ function toggleFavorite(productId: string) {
|
||||
}
|
||||
|
||||
.list-meta__sort {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.list-meta__sort-icon {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.list-meta__sort-badge {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
display: grid;
|
||||
grid-template-columns: 112px 1fr;
|
||||
@@ -415,7 +592,7 @@ function toggleFavorite(productId: string) {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border-radius: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ async function open(item: AppNotification) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProfileSubpageLayout title="اعلانها" subtitle="آخرین خبرهای سفارشهای شما">
|
||||
<ProfileSubpageLayout title="اعلانها" subtitle="سفارشها، پیشنهاد محصول و سایر اعلانها">
|
||||
<div v-if="items.length" class="notif-box">
|
||||
<div class="notif-box__head">
|
||||
<span class="notif-box__count">
|
||||
@@ -75,7 +75,7 @@ async function open(item: AppNotification) {
|
||||
v-else
|
||||
icon="vuesax-linear:notification-bing"
|
||||
title="اعلانی ندارید"
|
||||
message="وقتی فاکتور سفارشتان صادر شود یا وضعیت چک و پرداخت تغییر کند، همینجا خبردار میشوید."
|
||||
message="وقتی فاکتور سفارشتان صادر شود، پیشنهاد محصولی برایتان ثبت شود یا وضعیت پرداخت تغییر کند، همینجا خبردار میشوید."
|
||||
>
|
||||
<BaseButton variant="outline" @click="router.push('/orders')">سفارشهای من</BaseButton>
|
||||
</EmptyState>
|
||||
@@ -129,7 +129,7 @@ async function open(item: AppNotification) {
|
||||
}
|
||||
|
||||
.notif-row--unread {
|
||||
background: var(--color-elevated);
|
||||
background: var(--color-tab-inactive-bg);
|
||||
}
|
||||
|
||||
.notif-row__icon {
|
||||
|
||||
@@ -93,7 +93,7 @@ const ordersCount = computed(() => profile.value?.ordersCount ?? orderStore.orde
|
||||
const salesVolume = computed(() => profile.value?.salesVolumeToman ?? 0)
|
||||
const profit = computed(() => profile.value?.profitToman ?? 0)
|
||||
const levelProgress = computed(
|
||||
() => profile.value?.levelProgress ?? auth.user?.levelProgress ?? 72,
|
||||
() => profile.value?.levelProgress ?? auth.user?.levelProgress ?? 0,
|
||||
)
|
||||
const unreadLabel = computed(() =>
|
||||
notificationStore.unreadCount > 9 ? '9+' : String(notificationStore.unreadCount),
|
||||
@@ -257,9 +257,11 @@ onMounted(async () => {
|
||||
</svg>
|
||||
</div>
|
||||
<p class="loyalty__text">
|
||||
شما در سطح {{ levelLabel }} قرار دارید. تا سطح
|
||||
{{ nextLevelLabel }} فقط
|
||||
{{ formatNumber(pointsToNext) }} تومان فروش دیگر نیاز دارید.
|
||||
<span>شما در سطح {{ levelLabel }} قرار دارید.</span>
|
||||
<span class="loyalty__text-next">
|
||||
تا سطح {{ nextLevelLabel }} فقط
|
||||
{{ formatNumber(pointsToNext) }} تومان فروش دیگر نیاز دارید.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="loyalty__bar">
|
||||
@@ -605,11 +607,16 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.loyalty__text {
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.75;
|
||||
color: var(--color-text);
|
||||
text-align: start;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loyalty__text-next {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loyalty__bar {
|
||||
|
||||
@@ -288,7 +288,7 @@ const chartBars = computed(() => {
|
||||
|
||||
.btn-outline {
|
||||
border: 1px solid var(--color-border);
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user