feat(ui): B2 — счётчик «Сделки» в сайдбаре из API вместо хардкода

This commit is contained in:
Дмитрий
2026-05-17 03:14:13 +03:00
parent cefb71f5fa
commit 4b0809a82d
4 changed files with 66 additions and 3 deletions
+11
View File
@@ -233,3 +233,14 @@ export async function listProjects(tenantId: number): Promise<ApiProject[]> {
});
return data.projects;
}
/**
* Лёгкий count-only запрос для бейджа «Сделки» в AppSidebar (audit B2).
* Backend пропускает SELECT строк — отдаёт только COUNT(*).
*/
export async function fetchDealsCount(tenantId: number): Promise<number> {
const { data } = await apiClient.get<{ total: number }>('/api/deals', {
params: { tenant_id: tenantId, count_only: 1 },
});
return data.total;
}
@@ -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<boolean>('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<NavGroup[]>(() => [
{
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' },
+21
View File
@@ -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<number | null>(null);
async function load(tenantId: number): Promise<void> {
try {
count.value = await fetchDealsCount(tenantId);
} catch {
count.value = null;
}
}
return { count, load };
});
+24 -1
View File
@@ -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);
});