diff --git a/app/resources/js/api/deals.ts b/app/resources/js/api/deals.ts index 84d2c612..f043b28e 100644 --- a/app/resources/js/api/deals.ts +++ b/app/resources/js/api/deals.ts @@ -233,3 +233,14 @@ export async function listProjects(tenantId: number): Promise { }); return data.projects; } + +/** + * Лёгкий count-only запрос для бейджа «Сделки» в AppSidebar (audit B2). + * Backend пропускает SELECT строк — отдаёт только COUNT(*). + */ +export async function fetchDealsCount(tenantId: number): Promise { + const { data } = await apiClient.get<{ total: number }>('/api/deals', { + params: { tenant_id: tenantId, count_only: 1 }, + }); + return data.total; +} diff --git a/app/resources/js/components/layout/AppSidebar.vue b/app/resources/js/components/layout/AppSidebar.vue index 4b3fdac7..5a2e846a 100644 --- a/app/resources/js/components/layout/AppSidebar.vue +++ b/app/resources/js/components/layout/AppSidebar.vue @@ -7,9 +7,11 @@ * Brand mark + nav-tree (3 группы: Работа, Финансы, Команда). * Counts для «Сделки» — mock. */ -import { computed } from 'vue'; +import { computed, onMounted } from 'vue'; import { useRoute } from 'vue-router'; import Kbd from '../ui/Kbd.vue'; +import { useAuthStore } from '../../stores/auth'; +import { useDealsCountStore } from '../../stores/dealsCount'; interface NavItem { title: string; @@ -26,13 +28,19 @@ interface NavGroup { const drawerOpen = defineModel('drawerOpen', { default: true }); const route = useRoute(); +const auth = useAuthStore(); +const dealsCount = useDealsCountStore(); + +onMounted(() => { + if (auth.user?.tenant_id) void dealsCount.load(auth.user.tenant_id); +}); const navGroups = computed(() => [ { eyebrow: 'Работа', items: [ { title: 'Проекты', icon: 'mdi-folder-multiple-outline', to: '/projects' }, - { title: 'Сделки', icon: 'mdi-format-list-bulleted', to: '/deals', count: 247 }, + { title: 'Сделки', icon: 'mdi-format-list-bulleted', to: '/deals', count: dealsCount.count ?? undefined, countKey: 'deals' }, { title: 'Канбан', icon: 'mdi-view-column-outline', to: '/kanban' }, { title: 'Дашборд', icon: 'mdi-view-dashboard-outline', to: '/dashboard' }, { title: 'Импорт данных', icon: 'mdi-database-import-outline', to: '/import' }, diff --git a/app/resources/js/stores/dealsCount.ts b/app/resources/js/stores/dealsCount.ts new file mode 100644 index 00000000..86961ca5 --- /dev/null +++ b/app/resources/js/stores/dealsCount.ts @@ -0,0 +1,21 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { fetchDealsCount } from '../api/deals'; + +/** + * Счётчик сделок tenant'а для бейджа «Сделки» в AppSidebar (audit B2). + * count=null до загрузки или на fail → бейдж скрыт (resolveCount → 0). + */ +export const useDealsCountStore = defineStore('dealsCount', () => { + const count = ref(null); + + async function load(tenantId: number): Promise { + try { + count.value = await fetchDealsCount(tenantId); + } catch { + count.value = null; + } + } + + return { count, load }; +}); diff --git a/app/tests/Frontend/AppSidebarRedesign.spec.ts b/app/tests/Frontend/AppSidebarRedesign.spec.ts index 7a7622c5..69a787f4 100644 --- a/app/tests/Frontend/AppSidebarRedesign.spec.ts +++ b/app/tests/Frontend/AppSidebarRedesign.spec.ts @@ -1,12 +1,15 @@ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect, beforeEach, test } from 'vitest'; import { mount, type VueWrapper } from '@vue/test-utils'; import { createMemoryHistory, createRouter, type Router } from 'vue-router'; import { createPinia, setActivePinia } from 'pinia'; import { createVuetify } from 'vuetify'; import AppSidebar from '../../resources/js/components/layout/AppSidebar.vue'; +import { useDealsCountStore } from '../../resources/js/stores/dealsCount'; async function setup(initialRoute = '/deals'): Promise<{ wrapper: VueWrapper; router: Router }> { setActivePinia(createPinia()); + // B2: default count=5 so badge renders in non-B2 tests (replaces hardcoded 247). + useDealsCountStore().count = 5; const router = createRouter({ history: createMemoryHistory(), routes: [ @@ -63,3 +66,23 @@ describe('AppSidebar — redesigned shell', () => { expect(active).toBeDefined(); }); }); + +test('B2: бейдж «Сделки» рендерит count из dealsCount-store', async () => { + const { wrapper } = await setup(); + const store = useDealsCountStore(); + store.count = 42; + await wrapper.vm.$nextTick(); + + const badge = wrapper.find('[data-testid="nav-count-deals"]'); + expect(badge.exists()).toBe(true); + expect(badge.text()).toBe('42'); +}); + +test('B2: бейдж «Сделки» скрыт пока count=null', async () => { + const { wrapper } = await setup(); + const store = useDealsCountStore(); + store.count = null; + await wrapper.vm.$nextTick(); + + expect(wrapper.find('[data-testid="nav-count-deals"]').exists()).toBe(false); +});