2f6e88e784
Три правки после демо Этапа 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>
86 lines
3.8 KiB
TypeScript
86 lines
3.8 KiB
TypeScript
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(['С_ДАТОЙ', 'БЕЗ_ДАТЫ']);
|
||
});
|
||
});
|