Files
nademitex_user/src/views/profile/components/DesktopProfilePage.vue
T

750 lines
19 KiB
Vue
Raw Normal View History

<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>