Files
nademitex_user/src/views/profile/components/DesktopProfilePage.vue
T
safavimohadeseh78andCursor f252749d18 feat(auth): referrer code at signup and agent code in profile
Optional referrer agent code in registration with field validation UX. Show read-only agent referral code in profile edit and desktop overview.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-04 10:38:16 +03:30

750 lines
19 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 UserAvatar from '@/components/ui/UserAvatar.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?.id) return 0
return orderStore.userOrders(auth.user.id).length
})
const checksCount = computed(() => {
if (!auth.user?.id) 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(() => {
const u = auth.user
if (!u) return toPersianDigits('—')
if (auth.isAgent) {
return u.agentCode ? toPersianDigits(u.agentCode) : toPersianDigits('—')
}
if (u.nationalId) return toPersianDigits(u.nationalId.slice(-6).padStart(6, '0'))
return toPersianDigits(u.id?.slice(-6) ?? '—')
})
const codeLabel = computed(() => (auth.isAgent ? 'کد واسطه:' : 'کد مشتری:'))
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(() => 0)
const companyRows = computed(() => {
const rows: Array<{ label: string; value: string; badge?: boolean }> = [
{ 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: auth.isAgent ? 'واسطه' : 'مشتری عمده',
badge: true,
},
{ label: 'وضعیت حساب', value: 'فعال', badge: true },
]
if (auth.isAgent) {
rows.splice(1, 0, {
label: 'کد واسطه',
value: auth.user?.agentCode
? toPersianDigits(auth.user.agentCode)
: '—',
})
}
return rows
})
const quickAccess = [
{ label: 'حساب‌های بانکی', icon: IconCashSettlement },
{ label: 'آدرس‌ها', icon: IconMapPin, path: '/profile/addresses' },
{ 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/security',
},
{
id: 'notifications',
label: 'اعلان‌ها',
description: 'خبرهای فاکتور، چک و ارسال سفارش',
icon: IconBell,
path: '/profile/notifications',
},
{
id: 'display',
label: 'تنظیمات',
description: 'نمایش، اطلاعات کاربری و خروج',
icon: IconLayoutGrid,
path: '/profile/display',
},
{
id: 'logout',
label: 'خروج از حساب',
description: 'پایان نشست و ورود مجدد',
icon: IconLock,
action: 'logout',
},
]
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">
<UserAvatar
class="profile-overview__avatar"
:src="auth.user?.avatarUrl"
:initials="initials"
:size="92"
/>
<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">{{ codeLabel }}</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 {
width: 100%;
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 {
background: var(--color-primary);
color: var(--color-surface);
}
.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: var(--color-surface);
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: var(--color-text-muted);
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: var(--color-warning-light);
color: var(--color-warning);
}
.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 var(--color-border);
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 var(--color-border);
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: var(--color-elevated);
cursor: pointer;
font-family: inherit;
transition: border-color 0.15s, background 0.15s;
}
.profile-quick-grid__item:hover {
border-color: var(--color-primary);
background: var(--color-primary-soft);
}
.profile-quick-grid__icon {
width: 42px;
height: 42px;
border-radius: 12px;
background: var(--color-surface);
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 var(--color-border);
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: var(--color-elevated);
}
.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>