Files
portal/app/tests/Frontend/SalesProspectBoard.spec.ts
T
Дмитрий 2f6e88e784 feat(sales): замечания владельца по воронке — богатая карточка, сортировка созвонов, счётчики фильтра
Три правки после демо Этапа 1 (все по TDD):
1. Карточка показывает ВСЕ данные поиска из payload (тип ProspectPayload 1:1 с
   dataclass Firm; computed infoRows рендерит юрлицо, директора+личный ИНН,
   контакты, бюджет Директа вилкой, каналы/коллтрекинг, оценку). Демо-сидер
   кладёт полный синтетический payload.
2. Начальнику отдаётся manager_counts; фильтр показывает «Имя (N)».
3. Колонка «Переговоры» сортируется по next_call_at ASC (просроченные сверху).

Гейты: бэк 12/12, фронт 19/19, Larastan 0. НЕ выкачено (ждём разрешения владельца).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:52:54 +03:00

86 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { mount } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { describe, expect, it } from 'vitest';
import SalesProspectBoard from '../../resources/js/components/sales/SalesProspectBoard.vue';
import type { SalesProspect } from '../../resources/js/api/sales';
const vuetify = createVuetify();
function p(id: number, stage: string, overrides: Partial<SalesProspect> = {}): SalesProspect {
return {
id,
sales_user_id: 1,
stage,
firm_name: `Фирма ${id}`,
city: 'Ростов',
phone: null,
site: null,
inn: null,
rating_label: 'горячая',
payload: {},
next_call_at: null,
reason: null,
registered_email: null,
notes: null,
...overrides,
};
}
describe('SalesProspectBoard', () => {
it('рисует 8 колонок и раскладывает карточки по стадиям', () => {
const w = mount(SalesProspectBoard, {
props: { prospects: [p(1, 'new'), p(2, 'negotiation'), p(3, 'new')] },
global: { plugins: [vuetify] },
});
expect(w.findAll('.prospect-column').length).toBe(8);
expect(w.text()).toContain('Новые');
expect(w.text()).toContain('Фирма 1');
});
it('клик по карточке эмитит open с прогнозом', async () => {
const w = mount(SalesProspectBoard, { props: { prospects: [p(5, 'new')] }, global: { plugins: [vuetify] } });
await w.find('.prospect-card').trigger('click');
expect(w.emitted('open')?.[0]?.[0]).toMatchObject({ id: 5 });
});
it('просроченный созвон подсвечивается классом overdue', () => {
const w = mount(SalesProspectBoard, {
props: { prospects: [p(9, 'negotiation', { next_call_at: '2000-01-01T00:00:00+03:00' })] },
global: { plugins: [vuetify] },
});
expect(w.find('.prospect-card.overdue').exists()).toBe(true);
});
it('колонка «Переговоры» сортируется по ближайшему созвону (ASC, просроченные сверху)', () => {
// id по убыванию (как отдаёт API), но даты созвона вперемешку.
const w = mount(SalesProspectBoard, {
props: {
prospects: [
p(30, 'negotiation', { firm_name: 'ПОЗЖЕ', next_call_at: '2026-08-01T10:00:00+03:00' }),
p(20, 'negotiation', { firm_name: 'ПРОСРОЧЕНО', next_call_at: '2000-01-01T00:00:00+03:00' }),
p(10, 'negotiation', { firm_name: 'СКОРО', next_call_at: '2026-07-16T10:00:00+03:00' }),
],
},
global: { plugins: [vuetify] },
});
const negColumn = w.findAll('.prospect-column')[1]; // 0=Новые, 1=Переговоры
const names = negColumn.findAll('.prospect-card-name').map((n) => n.text());
expect(names).toEqual(['ПРОСРОЧЕНО', 'СКОРО', 'ПОЗЖЕ']);
});
it('карточки «Переговоры» без даты созвона — в конце колонки', () => {
const w = mount(SalesProspectBoard, {
props: {
prospects: [
p(40, 'negotiation', { firm_name: 'БЕЗ_ДАТЫ', next_call_at: null }),
p(41, 'negotiation', { firm_name: 'С_ДАТОЙ', next_call_at: '2026-07-16T10:00:00+03:00' }),
],
},
global: { plugins: [vuetify] },
});
const negColumn = w.findAll('.prospect-column')[1];
const names = negColumn.findAll('.prospect-card-name').map((n) => n.text());
expect(names).toEqual(['С_ДАТОЙ', 'БЕЗ_ДАТЫ']);
});
});