Improve Leads mobile UX and nest leads under Customers nav.

Restore the 5-tab mobile bar, add responsive lead cards with long-press select, and fix stats/filters layout.
This commit is contained in:
2026-09-28 11:50:44 +03:30
parent 7e73ba9936
commit 8effd4cbc6
4 changed files with 727 additions and 108 deletions
+83 -32
View File
@@ -32,6 +32,8 @@ const SIDEBAR_KEY = 'mento_sidebar_collapsed';
const collapsed = ref(false);
const openHubKey = ref<NavHubKey | null>(null);
const hubStack = ref<NavHubKey[]>([]);
const isMobileNav = ref(false);
const globalSearch = ref('');
const activityBadge = ref(0);
@@ -44,10 +46,12 @@ function pathIn(prefixes: string[]) {
const dashboardActive = computed(() => route.path === '/');
const loyaltyActive = computed(() => pathIn(['/loyalty']));
const reportsActive = computed(() => pathIn(['/reports']));
const customersActive = computed(() => pathIn(['/contacts']));
const leadsActive = computed(() =>
pathIn(['/leads', '/settings/lead-stages', '/settings/lead-forms']),
);
const customersActive = computed(
() => pathIn(['/contacts']) || (isMobileNav.value && leadsActive.value),
);
const salesActive = computed(() =>
pathIn([
'/pipeline',
@@ -111,23 +115,6 @@ function moduleAllowed(module?: string) {
return enabledModules.value.includes(module as never);
}
const openHub = computed<NavHubDef | null>(() => {
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) && moduleAllowed(card.module),
),
shortcuts: hub.shortcuts.filter(
(item) =>
(!item.admin || canSettings.value) && moduleAllowed(item.module),
),
};
});
const hubVisible = computed(() => {
const map: Record<string, boolean> = {};
for (const hub of NAV_HUBS) {
@@ -139,6 +126,41 @@ const hubVisible = computed(() => {
return map;
});
const openHub = computed<NavHubDef | null>(() => {
if (!openHubKey.value) return null;
const hub = NAV_HUBS.find((item) => item.key === openHubKey.value);
if (!hub) return null;
const cards = hub.cards.filter(
(card) =>
(!card.admin || canSettings.value) && moduleAllowed(card.module),
);
if (
hub.key === 'customers' &&
isMobileNav.value &&
hubVisible.value.leads
) {
cards.push({
to: '/leads',
hub: 'leads',
title: 'لیدها',
desc: 'مخاطبین بالقوه قبل از تبدیل به مشتری',
icon: 'user-plus',
tint: 'sky',
module: 'leads',
});
}
return {
...hub,
cards,
shortcuts: hub.shortcuts.filter(
(item) =>
(!item.admin || canSettings.value) && moduleAllowed(item.module),
),
};
});
const hubCanBack = computed(() => hubStack.value.length > 0);
function givenName(fullName: string): string {
const parts = fullName.trim().split(/\s+/).filter(Boolean);
if (parts.length <= 1) return fullName.trim();
@@ -177,12 +199,31 @@ watch([profileFullName, collapsed], () => {
function closeHub() {
openHubKey.value = null;
hubStack.value = [];
}
function onHubClick(key: NavHubKey) {
hubStack.value = [];
openHubKey.value = openHubKey.value === key ? null : key;
}
function openNestedHub(key: NavHubKey) {
if (openHubKey.value) {
hubStack.value = [...hubStack.value, openHubKey.value];
}
openHubKey.value = key;
}
function backHub() {
const prev = hubStack.value[hubStack.value.length - 1];
if (!prev) {
closeHub();
return;
}
hubStack.value = hubStack.value.slice(0, -1);
openHubKey.value = prev;
}
function openTabHub(key: NavHubKey) {
moreOpen.value = false;
notifOpen.value = false;
@@ -233,7 +274,11 @@ function closeNotif() {
}
function onMobileMqChange(event: MediaQueryListEvent) {
if (!event.matches) closeMore();
isMobileNav.value = event.matches;
if (!event.matches) {
closeMore();
hubStack.value = [];
}
}
function onLogout() {
@@ -358,6 +403,7 @@ onMounted(() => {
if (meta) profileNameObserver.observe(meta);
}
mobileMq = window.matchMedia('(max-width: 900px)');
isMobileNav.value = mobileMq.matches;
mobileMq.addEventListener('change', onMobileMqChange);
});
@@ -717,7 +763,14 @@ onUnmounted(() => {
<div v-if="openHub" class="hub-layer">
<button class="hub-dim" type="button" aria-label="بستن منو" @click="closeHub" />
<NavHubPanel :hub="openHub" @close="closeHub" @go="goHub" />
<NavHubPanel
:hub="openHub"
:can-back="hubCanBack"
@close="closeHub"
@back="backHub"
@go="goHub"
@open-hub="openNestedHub"
/>
</div>
<nav class="mobile-tabbar mobile-only" aria-label="منوی اصلی">
@@ -733,23 +786,21 @@ onUnmounted(() => {
<button
class="tab"
type="button"
:class="{ on: !moreOpen && (customersActive || openHubKey === 'customers') }"
:aria-expanded="openHubKey === 'customers'"
:class="{
on:
!moreOpen &&
(customersActive ||
openHubKey === 'customers' ||
(isMobileNav && openHubKey === 'leads')),
}"
:aria-expanded="
openHubKey === 'customers' || (isMobileNav && openHubKey === 'leads')
"
@click="openTabHub('customers')"
>
<UiIcon name="users" :size="20" />
<span>مشتریان</span>
</button>
<button
class="tab"
type="button"
:class="{ on: !moreOpen && (leadsActive || openHubKey === 'leads') }"
:aria-expanded="openHubKey === 'leads'"
@click="openTabHub('leads')"
>
<UiIcon name="user-plus" :size="20" />
<span>لیدها</span>
</button>
<button
class="tab"
type="button"