e9edb206c8
feat(sales): кабинет начальника — единый период (без 500) + пополнения баланса Экраны начальника отстали от вчерашних правок кабинета менеджера (§16–§21). 1. Живой баг: «Произвольный» период до выбора дат ронял запрос в 500 на пяти точках из шести (сводка отдела, доход, результативность, выплаты, тарифы). Разбор периода вынесен в трейт ResolvesSalesPeriod: 422 вместо падения, период по умолчанию d30 вместо this — как показывает сам PeriodPicker. 2. Пополнения баланса (topup_rub) добавлены в dashboard/overview (kpi + строки менеджеров) и managers/performance. На экранах: плашка «Пополнили баланс» в сводке отдела и колонка «Пополнили ₽» в обеих таблицах результативности. Из подписей убрано «(мес)» — период больше не месяц. 3. «Воронка отдела» правок не потребовала: она рендерит те же доску и карточку, что экран менеджера, а карточка сама грузит журнал и сохраняет контакты. Тесты: SalesPeriodRequestTest проходит по всем шести точкам сразу. Pest 263/263, Vitest 1420/1420, vue-tsc чист. Спека — §22. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
265 lines
11 KiB
TypeScript
265 lines
11 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { mount, flushPromises } from '@vue/test-utils';
|
|
import { createVuetify } from 'vuetify';
|
|
import { createPinia } from 'pinia';
|
|
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
import SalesBossOverviewView from '../../resources/js/views/sales/SalesBossOverviewView.vue';
|
|
|
|
// ─── mock getSalesDashboardOverview ────────────────────────────────────────────
|
|
|
|
// NOTE: vi.mock is hoisted, so the factory must not reference outer variables.
|
|
vi.mock('../../resources/js/api/sales', () => ({
|
|
getSalesDashboardOverview: vi.fn().mockResolvedValue({
|
|
kpi: {
|
|
managers_count: 4,
|
|
managers_active: 3,
|
|
managers_vacation: 1,
|
|
clients_count: 16,
|
|
balance_sum_rub: '1248600',
|
|
oborot_rub: 508950,
|
|
topup_rub: 612300,
|
|
paid_period_rub: 125000,
|
|
},
|
|
alerts: {
|
|
invoices_awaiting: 2,
|
|
attachments_pending: 3,
|
|
clients_balance_problem: 5,
|
|
},
|
|
performance: [
|
|
{
|
|
manager_id: 11,
|
|
name: 'Алексей Смирнов',
|
|
clients_count: 7,
|
|
leads_delivered: 1567,
|
|
oborot_rub: 235050,
|
|
topup_rub: 301000,
|
|
paid_all_time_rub: 85000,
|
|
earned_rub: 13350,
|
|
},
|
|
{
|
|
manager_id: 12,
|
|
name: 'Мария Котова',
|
|
clients_count: 5,
|
|
leads_delivered: 1102,
|
|
oborot_rub: 165300,
|
|
topup_rub: 208500,
|
|
paid_all_time_rub: 60000,
|
|
earned_rub: 54795,
|
|
},
|
|
{
|
|
manager_id: 13,
|
|
name: 'Игорь Лебедев',
|
|
clients_count: 4,
|
|
leads_delivered: 724,
|
|
oborot_rub: 108600,
|
|
topup_rub: 102800,
|
|
paid_all_time_rub: 40000,
|
|
earned_rub: 10000,
|
|
},
|
|
],
|
|
}),
|
|
}));
|
|
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
function makeRouter() {
|
|
const router = createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [
|
|
{ path: '/sales/boss', name: 'sales-boss', component: SalesBossOverviewView },
|
|
{ path: '/sales/invoices', name: 'sales-invoices', component: { template: '<div/>' } },
|
|
{ path: '/sales/requests', name: 'sales-requests', component: { template: '<div/>' } },
|
|
{ path: '/sales/performance', name: 'sales-performance', component: { template: '<div/>' } },
|
|
{ path: '/sales/payouts', name: 'sales-payouts', component: { template: '<div/>' } },
|
|
],
|
|
});
|
|
return router.push('/sales/boss').then(() => router);
|
|
}
|
|
|
|
describe('SalesBossOverviewView', () => {
|
|
it('вызывает getSalesDashboardOverview при монтировании', async () => {
|
|
const router = await makeRouter();
|
|
mount(SalesBossOverviewView, {
|
|
global: { plugins: [createVuetify(), createPinia(), router] },
|
|
});
|
|
await flushPromises();
|
|
|
|
const api = await import('../../resources/js/api/sales');
|
|
expect(api.getSalesDashboardOverview).toHaveBeenCalled();
|
|
});
|
|
|
|
it('отрисовывает KPI — Менеджеров с подписью активны/в отпуске', async () => {
|
|
const router = await makeRouter();
|
|
const wrapper = mount(SalesBossOverviewView, {
|
|
global: { plugins: [createVuetify(), createPinia(), router] },
|
|
});
|
|
await flushPromises();
|
|
|
|
const kpi = wrapper.find('[data-testid="kpi-managers"]');
|
|
expect(kpi.text()).toContain('4');
|
|
expect(kpi.text()).toContain('3 активны');
|
|
expect(kpi.text()).toContain('1 в отпуске');
|
|
});
|
|
|
|
it('отрисовывает KPI — Клиентов закреплено', async () => {
|
|
const router = await makeRouter();
|
|
const wrapper = mount(SalesBossOverviewView, {
|
|
global: { plugins: [createVuetify(), createPinia(), router] },
|
|
});
|
|
await flushPromises();
|
|
|
|
expect(wrapper.find('[data-testid="kpi-clients"]').text()).toContain('16');
|
|
});
|
|
|
|
it('отрисовывает KPI — Σ баланс клиентов', async () => {
|
|
const router = await makeRouter();
|
|
const wrapper = mount(SalesBossOverviewView, {
|
|
global: { plugins: [createVuetify(), createPinia(), router] },
|
|
});
|
|
await flushPromises();
|
|
|
|
const kpi = wrapper.find('[data-testid="kpi-balance"]');
|
|
expect(kpi.text()).toContain('₽');
|
|
expect(kpi.text()).toContain('248');
|
|
});
|
|
|
|
it('отрисовывает KPI — Оборот отдела', async () => {
|
|
const router = await makeRouter();
|
|
const wrapper = mount(SalesBossOverviewView, {
|
|
global: { plugins: [createVuetify(), createPinia(), router] },
|
|
});
|
|
await flushPromises();
|
|
|
|
const kpi = wrapper.find('[data-testid="kpi-oborot"]');
|
|
expect(kpi.text()).toContain('508');
|
|
expect(kpi.text()).toContain('₽');
|
|
});
|
|
|
|
it('отрисовывает KPI — Выплачено менеджерам', async () => {
|
|
const router = await makeRouter();
|
|
const wrapper = mount(SalesBossOverviewView, {
|
|
global: { plugins: [createVuetify(), createPinia(), router] },
|
|
});
|
|
await flushPromises();
|
|
|
|
const kpi = wrapper.find('[data-testid="kpi-paid"]');
|
|
expect(kpi.text()).toContain('125');
|
|
expect(kpi.text()).toContain('₽');
|
|
});
|
|
|
|
it('отрисовывает 4 плитки-алерта с числами', async () => {
|
|
const router = await makeRouter();
|
|
const wrapper = mount(SalesBossOverviewView, {
|
|
global: { plugins: [createVuetify(), createPinia(), router] },
|
|
});
|
|
await flushPromises();
|
|
|
|
const cards = wrapper.findAll('[data-alert-card]');
|
|
expect(cards.length).toBe(4);
|
|
|
|
expect(wrapper.find('[data-testid="alert-invoices"]').text()).toContain('2');
|
|
expect(wrapper.find('[data-testid="alert-invoices"]').text()).toContain('счёта ждут оплаты');
|
|
expect(wrapper.find('[data-testid="alert-requests"]').text()).toContain('3');
|
|
expect(wrapper.find('[data-testid="alert-requests"]').text()).toContain('заявки на привязку');
|
|
expect(wrapper.find('[data-testid="alert-balance"]').text()).toContain('5');
|
|
expect(wrapper.find('[data-testid="alert-balance"]').text()).toContain('баланса');
|
|
expect(wrapper.find('[data-testid="alert-payout"]').text()).toContain('провести выплату');
|
|
});
|
|
|
|
it('клик по плитке «счета ждут оплаты» ведёт на /sales/invoices', async () => {
|
|
const router = await makeRouter();
|
|
const pushSpy = vi.spyOn(router, 'push');
|
|
const wrapper = mount(SalesBossOverviewView, {
|
|
global: { plugins: [createVuetify(), createPinia(), router] },
|
|
});
|
|
await flushPromises();
|
|
|
|
await wrapper.find('[data-testid="alert-invoices"]').trigger('click');
|
|
expect(pushSpy).toHaveBeenCalledWith('/sales/invoices');
|
|
});
|
|
|
|
it('клик по плитке «заявки на привязку» ведёт на /sales/requests', async () => {
|
|
const router = await makeRouter();
|
|
const pushSpy = vi.spyOn(router, 'push');
|
|
const wrapper = mount(SalesBossOverviewView, {
|
|
global: { plugins: [createVuetify(), createPinia(), router] },
|
|
});
|
|
await flushPromises();
|
|
|
|
await wrapper.find('[data-testid="alert-requests"]').trigger('click');
|
|
expect(pushSpy).toHaveBeenCalledWith('/sales/requests');
|
|
});
|
|
|
|
it('клик по плитке «проблема баланса» ведёт на /sales/performance', async () => {
|
|
const router = await makeRouter();
|
|
const pushSpy = vi.spyOn(router, 'push');
|
|
const wrapper = mount(SalesBossOverviewView, {
|
|
global: { plugins: [createVuetify(), createPinia(), router] },
|
|
});
|
|
await flushPromises();
|
|
|
|
await wrapper.find('[data-testid="alert-balance"]').trigger('click');
|
|
expect(pushSpy).toHaveBeenCalledWith('/sales/performance');
|
|
});
|
|
|
|
it('клик по плитке «провести выплату» ведёт на /sales/payouts', async () => {
|
|
const router = await makeRouter();
|
|
const pushSpy = vi.spyOn(router, 'push');
|
|
const wrapper = mount(SalesBossOverviewView, {
|
|
global: { plugins: [createVuetify(), createPinia(), router] },
|
|
});
|
|
await flushPromises();
|
|
|
|
await wrapper.find('[data-testid="alert-payout"]').trigger('click');
|
|
expect(pushSpy).toHaveBeenCalledWith('/sales/payouts');
|
|
});
|
|
|
|
it('таблица результативности менеджеров отрисовывает строки', async () => {
|
|
const router = await makeRouter();
|
|
const wrapper = mount(SalesBossOverviewView, {
|
|
global: { plugins: [createVuetify(), createPinia(), router] },
|
|
});
|
|
await flushPromises();
|
|
|
|
const rows = wrapper.findAll('[data-testid="performance-row"]');
|
|
expect(rows.length).toBe(3);
|
|
expect(rows[0].text()).toContain('Алексей Смирнов');
|
|
expect(rows[1].text()).toContain('Мария Котова');
|
|
expect(rows[2].text()).toContain('Игорь Лебедев');
|
|
// числовые ячейки первой строки
|
|
expect(rows[0].text()).toContain('7');
|
|
expect(rows[0].text()).toContain('235');
|
|
expect(rows[0].text()).toContain('13');
|
|
});
|
|
});
|
|
|
|
describe('SalesBossOverviewView — пополнения за период', () => {
|
|
it('показывает плашку «Пополнили баланс» с суммой за период', async () => {
|
|
const router = await makeRouter();
|
|
const w = mount(SalesBossOverviewView, {
|
|
global: { plugins: [createVuetify(), createPinia(), router] },
|
|
});
|
|
await flushPromises();
|
|
|
|
const tile = w.find('[data-testid="kpi-topup"]');
|
|
expect(tile.exists()).toBe(true);
|
|
expect(tile.text()).toContain('Пополнили баланс');
|
|
expect(tile.text().replace(/\s/g, '')).toContain('612300');
|
|
});
|
|
|
|
it('в таблице результативности есть колонка пополнений по каждому менеджеру', async () => {
|
|
const router = await makeRouter();
|
|
const w = mount(SalesBossOverviewView, {
|
|
global: { plugins: [createVuetify(), createPinia(), router] },
|
|
});
|
|
await flushPromises();
|
|
|
|
expect(w.find('[data-testid="performance-table"]').text()).toContain('Пополнили');
|
|
|
|
const first = w.findAll('[data-testid="performance-row"]')[0];
|
|
expect(first.text().replace(/\s/g, '')).toContain('301000');
|
|
});
|
|
});
|