diff --git a/apps/web/src/components/nav/NavHubPanel.vue b/apps/web/src/components/nav/NavHubPanel.vue new file mode 100644 index 0000000..926f393 --- /dev/null +++ b/apps/web/src/components/nav/NavHubPanel.vue @@ -0,0 +1,280 @@ + + + + + diff --git a/apps/web/src/layouts/AppLayout.vue b/apps/web/src/layouts/AppLayout.vue index f34333c..f2635d5 100644 --- a/apps/web/src/layouts/AppLayout.vue +++ b/apps/web/src/layouts/AppLayout.vue @@ -8,6 +8,8 @@ import UiIcon from '@/components/UiIcon.vue'; import MentoLogo from '@/components/brand/MentoLogo.vue'; import QuickCreate from '@/components/QuickCreate.vue'; import BrandName from '@/components/brand/BrandName.vue'; +import NavHubPanel from '@/components/nav/NavHubPanel.vue'; +import { NAV_HUBS, type NavHubDef, type NavHubKey } from '@/lib/nav-hubs'; import type { ActivitySummaryDto, NotificationDto, @@ -29,17 +31,10 @@ const todayLabel = computed(() => formatJalaliShort(new Date())); const SIDEBAR_KEY = 'mento_sidebar_collapsed'; const collapsed = ref(false); -const customersOpen = ref(false); -const salesOpen = ref(false); -const activitiesOpen = ref(false); -const supportOpen = ref(false); -const toolsOpen = ref(false); -const settingsOpen = ref(false); +const openHubKey = ref(null); const globalSearch = ref(''); const activityBadge = ref(0); -type FoldKey = 'customers' | 'sales' | 'activities' | 'support' | 'tools' | 'settings'; - function pathIn(prefixes: string[]) { return prefixes.some( (p) => route.path === p || route.path.startsWith(`${p}/`), @@ -66,18 +61,16 @@ const settingsActive = computed(() => '/settings/faq', ]), ); -const peopleContactsActive = computed( - () => customersActive.value && route.query.tab !== 'companies', -); -const companyContactsActive = computed( - () => customersActive.value && route.query.tab === 'companies', -); -const myActivitiesActive = computed( - () => - route.path === '/activities' || - route.path.startsWith('/activities/new') || - (route.path.startsWith('/activities/') && !route.path.startsWith('/activities/calendar')), -); +const openHub = computed(() => { + if (!openHubKey.value) return null; + const hub = NAV_HUBS.find((item) => item.key === openHubKey.value); + if (!hub) return null; + return { + ...hub, + cards: hub.cards.filter((card) => !card.admin || canSettings.value), + shortcuts: hub.shortcuts.filter((item) => !item.admin || canSettings.value), + }; +}); function givenName(fullName: string): string { const parts = fullName.trim().split(/\s+/).filter(Boolean); @@ -115,37 +108,19 @@ watch([profileFullName, collapsed], () => { void fitProfileName(); }); -watch( - () => route.path, - () => { - if (customersActive.value) customersOpen.value = true; - if (salesActive.value) salesOpen.value = true; - if (activitiesActive.value) activitiesOpen.value = true; - if (supportActive.value) supportOpen.value = true; - if (toolsActive.value) toolsOpen.value = true; - if (settingsActive.value) settingsOpen.value = true; - }, - { immediate: true }, -); - -function toggleFold(key: FoldKey) { - if (key === 'customers') customersOpen.value = !customersOpen.value; - if (key === 'sales') salesOpen.value = !salesOpen.value; - if (key === 'activities') activitiesOpen.value = !activitiesOpen.value; - if (key === 'support') supportOpen.value = !supportOpen.value; - if (key === 'tools') toolsOpen.value = !toolsOpen.value; - if (key === 'settings') settingsOpen.value = !settingsOpen.value; +function closeHub() { + openHubKey.value = null; } -function onFoldClick(key: FoldKey, fallback: string, event: Event) { - event.preventDefault(); - event.stopPropagation(); - if (collapsed.value) { - void router.push(fallback); - closeMenu(); - return; - } - toggleFold(key); +function onHubClick(key: NavHubKey) { + openHubKey.value = openHubKey.value === key ? null : key; + closeMenu(); +} + +function goHub(to: string) { + closeHub(); + closeMenu(); + void router.push(to); } const menuOpen = ref(false); @@ -175,6 +150,7 @@ function closeNotif() { } function onLogout() { + closeHub(); closeMenu(); closeNotif(); auth.logout(); @@ -254,17 +230,19 @@ function submitSearch() { void router.push({ name: 'contacts', query: { q } }); } -watch(menuOpen, (open) => { - document.body.style.overflow = open || notifOpen.value ? 'hidden' : ''; -}); -watch(notifOpen, (open) => { - document.body.style.overflow = open || menuOpen.value ? 'hidden' : ''; -}); +function syncBodyLock() { + document.body.style.overflow = + menuOpen.value || notifOpen.value || Boolean(openHubKey.value) ? 'hidden' : ''; +} +watch(menuOpen, syncBodyLock); +watch(notifOpen, syncBodyLock); +watch(openHubKey, syncBodyLock); watch( () => route.fullPath, () => { closeMenu(); closeNotif(); + closeHub(); }, ); @@ -295,7 +273,7 @@ onUnmounted(() => { @@ -664,6 +585,7 @@ onUnmounted(() => { --sidebar-card: 220px; --sidebar-gutter-start: 14px; --sidebar-gutter-end: 10px; + --hub-offset: calc(var(--sidebar-card) + var(--sidebar-gutter-start)); display: grid; grid-template-columns: calc(var(--sidebar-card) + var(--sidebar-gutter-start) + var(--sidebar-gutter-end)) minmax(0, 1fr); min-height: 100vh; @@ -694,6 +616,7 @@ onUnmounted(() => { position: sticky; top: 14px; align-self: start; + z-index: 20; height: calc(100dvh - 28px); margin: 14px; margin-inline-end: 10px; @@ -806,6 +729,45 @@ onUnmounted(() => { color: #0f766e; } +.shell.hub-open .nav-item.router-link-active:not(.hub-on) { + background: transparent; + color: #334155; + box-shadow: none; +} + +.shell.hub-open .nav-item.router-link-active:not(.hub-on) :deep(.ico) { + color: inherit; +} + +.shell.hub-open .sidebar { + z-index: 70; +} + +.hub-layer { + position: fixed; + inset: 0; + z-index: 60; + pointer-events: none; +} + +.hub-dim { + pointer-events: auto; + position: absolute; + inset: 0; + border: 0; + padding: 0; + background: rgba(15, 23, 42, 0.28); + cursor: default; +} + +.hub-layer :deep(.hub) { + pointer-events: auto; + position: absolute; + top: 42px; + inset-inline-start: var(--hub-offset); + inset-inline-end: auto; +} + .nav-badge { margin-inline-start: auto; min-width: 1.35rem; @@ -858,13 +820,13 @@ onUnmounted(() => { height: 0.42rem; border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor; - transform: rotate(45deg); + transform: rotate(135deg); transition: transform 0.15s ease; flex-shrink: 0; } -.nav-fold.open > .nav-fold-btn .chev { - transform: rotate(-135deg); +.nav-fold-btn.hub-on .chev { + transform: rotate(-45deg); } .nav-sub { @@ -1108,6 +1070,7 @@ onUnmounted(() => { @media (min-width: 901px) { .shell.collapsed { + --hub-offset: 72px; grid-template-columns: 80px minmax(0, 1fr); } @@ -1650,6 +1613,28 @@ onUnmounted(() => { box-shadow: 0 0 40px rgba(0, 0, 0, 0.15); } + .hub-layer { + z-index: 80; + } + + .hub-layer :deep(.hub) { + top: auto; + bottom: 0; + inset-inline: 0; + width: 100%; + margin: 0; + animation: hub-sheet-up 0.28s ease; + } + + @keyframes hub-sheet-up { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } + } + .content { padding: 1rem 0.85rem 1.4rem; } diff --git a/apps/web/src/lib/nav-hubs.ts b/apps/web/src/lib/nav-hubs.ts new file mode 100644 index 0000000..04afb9a --- /dev/null +++ b/apps/web/src/lib/nav-hubs.ts @@ -0,0 +1,296 @@ +export type NavHubKey = + | 'customers' + | 'sales' + | 'activities' + | 'support' + | 'tools' + | 'settings'; + +export type NavIconName = + | 'users' + | 'user' + | 'building' + | 'briefcase' + | 'filter' + | 'document' + | 'box' + | 'mail' + | 'dollar' + | 'target' + | 'check' + | 'calendar' + | 'chat' + | 'apps-plus' + | 'layers' + | 'flag' + | 'settings' + | 'pencil' + | 'chart' + | 'lightbulb' + | 'plus' + | 'user-plus'; + +export type NavHubTint = 'mint' | 'violet' | 'peach' | 'sky' | 'rose' | 'amber'; + +export type NavHubCard = { + to: string; + title: string; + desc: string; + icon: NavIconName; + tint: NavHubTint; + admin?: boolean; +}; + +export type NavHubShortcut = { + to: string; + title: string; + icon: NavIconName; + admin?: boolean; +}; + +export type NavHubDef = { + key: NavHubKey; + title: string; + desc: string; + icon: NavIconName; + tint: NavHubTint; + cards: NavHubCard[]; + shortcuts: NavHubShortcut[]; +}; + +export const NAV_HUBS: NavHubDef[] = [ + { + key: 'customers', + title: 'مشتریان', + desc: 'اشخاص، شرکت‌ها و پرونده‌های ارتباطی', + icon: 'users', + tint: 'mint', + shortcuts: [ + { to: '/contacts/people/new', title: 'شخص جدید', icon: 'user-plus' }, + { to: '/contacts/companies/new', title: 'شرکت جدید', icon: 'building' }, + ], + cards: [ + { + to: '/contacts', + title: 'اشخاص', + desc: 'مخاطبان حقیقی و پرونده‌های مشتری', + icon: 'user', + tint: 'mint', + }, + { + to: '/contacts?tab=companies', + title: 'شرکت‌ها', + desc: 'سازمان‌ها و مشتریان حقوقی', + icon: 'building', + tint: 'violet', + }, + ], + }, + { + key: 'sales', + title: 'فروش', + desc: 'مدیریت چرخه فروش و درآمد', + icon: 'briefcase', + tint: 'mint', + shortcuts: [ + { to: '/deals/new', title: 'معامله جدید', icon: 'plus' }, + { to: '/quotes/new', title: 'پیش‌فاکتور جدید', icon: 'document' }, + ], + cards: [ + { + to: '/pipeline', + title: 'کاریز فروش', + desc: 'سرنخ‌ها، معاملات و مراحل فروش', + icon: 'filter', + tint: 'mint', + }, + { + to: '/settings/products', + title: 'محصولات و خدمات', + desc: 'مدیریت محصولات، قیمت‌ها و موجودی', + icon: 'box', + tint: 'peach', + admin: true, + }, + { + to: '/invoices', + title: 'فاکتورها', + desc: 'صدور فاکتور و پیگیری پرداخت‌ها', + icon: 'mail', + tint: 'sky', + }, + { + to: '/quotes', + title: 'پیش‌فاکتورها', + desc: 'پیشنهاد قیمت و پیگیری تایید', + icon: 'document', + tint: 'amber', + }, + { + to: '/commissions', + title: 'پورسانت', + desc: 'سهم فروشندگان از فاکتورهای صادرشده', + icon: 'dollar', + tint: 'rose', + }, + { + to: '/sales/forecast', + title: 'هدف و پیش‌بینی', + desc: 'تعیین اهداف و تحلیل عملکرد فروش', + icon: 'target', + tint: 'mint', + }, + ], + }, + { + key: 'activities', + title: 'فعالیت‌ها', + desc: 'کارها، تماس‌ها و برنامه‌ریزی روزانه', + icon: 'check', + tint: 'sky', + shortcuts: [ + { to: '/activities/new', title: 'فعالیت جدید', icon: 'plus' }, + { to: '/activities/calendar', title: 'تقویم', icon: 'calendar' }, + ], + cards: [ + { + to: '/activities', + title: 'فعالیت‌های من', + desc: 'کارها، تماس‌ها و پیگیری‌های جاری', + icon: 'check', + tint: 'mint', + }, + { + to: '/activities/calendar', + title: 'تقویم فعالیت‌ها', + desc: 'نمای روز، هفته و زمان‌بندی کارها', + icon: 'calendar', + tint: 'violet', + }, + ], + }, + { + key: 'support', + title: 'پشتیبانی', + desc: 'درخواست‌ها و گفتگو با مشتری', + icon: 'chat', + tint: 'peach', + shortcuts: [ + { to: '/tickets/new', title: 'تیکت جدید', icon: 'plus' }, + { to: '/tickets', title: 'تیکت‌ها', icon: 'chat' }, + ], + cards: [ + { + to: '/tickets', + title: 'تیکت‌ها', + desc: 'درخواست‌ها و پیام‌های پشتیبانی', + icon: 'chat', + tint: 'peach', + }, + ], + }, + { + key: 'tools', + title: 'ابزارها', + desc: 'فرم‌ها، پروژه، انبار و اسناد مالی', + icon: 'apps-plus', + tint: 'violet', + shortcuts: [ + { to: '/project-cards/new', title: 'کارت پروژه جدید', icon: 'plus' }, + { to: '/cheques', title: 'ثبت چک', icon: 'document' }, + ], + cards: [ + { + to: '/settings/lead-forms', + title: 'فرم‌ساز سرنخ', + desc: 'فرم عمومی برای جذب سرنخ تازه', + icon: 'layers', + tint: 'mint', + admin: true, + }, + { + to: '/projects', + title: 'کاریز پروژه', + desc: 'کارت‌ها و مراحل اجرای کار', + icon: 'flag', + tint: 'violet', + }, + { + to: '/inventory', + title: 'انبار', + desc: 'موجودی کالا و گردش داخلی', + icon: 'box', + tint: 'peach', + }, + { + to: '/cheques', + title: 'چک‌ها', + desc: 'چک‌های دریافتی و پرداختی', + icon: 'document', + tint: 'sky', + }, + { + to: '/attachments', + title: 'پیوست‌ها', + desc: 'فایل‌های متصل به یادداشت‌ها', + icon: 'document', + tint: 'amber', + }, + ], + }, + { + key: 'settings', + title: 'تنظیمات', + desc: 'سازمان، تیم و پیکربندی سیستم', + icon: 'settings', + tint: 'sky', + shortcuts: [ + { to: '/settings/team#settings-team', title: 'تیم و نقش‌ها', icon: 'users' }, + { to: '/settings/tahesab', title: 'ته‌حساب', icon: 'chart' }, + ], + cards: [ + { + to: '/settings/team#settings-org', + title: 'تنظیمات عمومی', + desc: 'نام سازمان و تنظیمات پایه', + icon: 'building', + tint: 'mint', + }, + { + to: '/settings/team#settings-team', + title: 'تیم و نقش‌ها', + desc: 'اعضا، دسترسی و نقش‌ها', + icon: 'users', + tint: 'violet', + }, + { + to: '/settings/custom-fields', + title: 'فیلدهای سفارشی', + desc: 'فیلدهای اضافه روی پرونده‌ها', + icon: 'pencil', + tint: 'peach', + }, + { + to: '/settings/currencies', + title: 'ارزها', + desc: 'ارز پایه و نرخ تبدیل', + icon: 'dollar', + tint: 'sky', + }, + { + to: '/settings/tahesab', + title: 'ته‌حساب', + desc: 'اتصال مالی و تنظیمات حسابداری', + icon: 'chart', + tint: 'amber', + }, + { + to: '/settings/faq', + title: 'FAQ', + desc: 'پرسش‌های پرتکرار سازمان', + icon: 'lightbulb', + tint: 'rose', + }, + ], + }, +]; diff --git a/apps/web/src/views/AutomationsSettingsView.vue b/apps/web/src/views/AutomationsSettingsView.vue index 4fc0a8c..31c4738 100644 --- a/apps/web/src/views/AutomationsSettingsView.vue +++ b/apps/web/src/views/AutomationsSettingsView.vue @@ -442,6 +442,10 @@ h2 { gap: 0.45rem; } +.rules ul:empty { + display: none; +} + .rules li { border: 1px solid #e8eef3; border-radius: 14px; @@ -599,7 +603,14 @@ h2 { .rules { grid-area: rules; - height: 100%; + /* Let the toolbar set the row height; don't let the rules card grow it. */ + height: 0; + min-height: 100%; + padding: 0.85rem 1rem; + overflow: hidden; + box-sizing: border-box; + gap: 0.35rem; + align-content: center; } .studio {