68428f16c6
feat(sales): карточка кандидата — девять полей владельца + правка контактных лиц
Владелец задал список полей карточки и попросил дать менеджеру записывать
контактных лиц прямо в карточке: поиск отдаёт 5-7 ПРЕДПОЛАГАЕМЫХ номеров
директора, живой из них выясняется обзвоном; а если директор перенаправил
к маркетологу — менеджер фиксирует и его, людей может быть 2-3.
- Карточка показывает РОВНО девять полей в его порядке: Юрлицо, Сайт, ИНН фирмы,
Адрес, Телефон общий, Каналы, Бюджет, Директор, Тел. директора (предполагаемый).
Убраны Оценка, Достоверность, Реклама, Запросы в Директе, ОГРН, Статус юрлица,
Личный ИНН директора, Почта директора — данные остаются в payload.
- PATCH /api/sales/prospects/{id}/contacts — список заменяется целиком, пустой
стирает всех; права как у update() (менеджер свои, начальник любые); главный
телефон карточки из поиска не трогается.
- ProspectContactsEditor.vue + utils/prospectContacts.ts — один редактор контактов
на диалог создания и карточку; диалог создания переведён на него.
- Закрывает хвост §16: у 24 живых карточек из поиска контактных лиц не было,
теперь дозаполняются руками.
Спека §18. Гейты: Pest 223/223 sales, Vitest 196 файлов / 1397 тестов,
vue-tsc чисто, Larastan 0 в своих файлах. TDD RED→GREEN на каждом шаге.
LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии
tests/Feature/Admin/*Balances*, свои файлы проверены отдельно и чисты.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
157 lines
5.3 KiB
TypeScript
157 lines
5.3 KiB
TypeScript
import { mount, flushPromises } from '@vue/test-utils';
|
|
import { createVuetify } from 'vuetify';
|
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
|
|
vi.mock('../../resources/js/api/sales', () => ({
|
|
lookupProspectInn: vi.fn(),
|
|
}));
|
|
|
|
import SalesProspectCreateDialog from '../../resources/js/components/sales/SalesProspectCreateDialog.vue';
|
|
import { lookupProspectInn } from '../../resources/js/api/sales';
|
|
|
|
const vuetify = createVuetify();
|
|
|
|
interface Contact {
|
|
name: string;
|
|
position: string;
|
|
phones: string[];
|
|
}
|
|
|
|
interface CreateVm {
|
|
firmName: string;
|
|
legalName: string;
|
|
city: string;
|
|
site: string;
|
|
inn: string;
|
|
notes: string;
|
|
contacts: Contact[];
|
|
error: string;
|
|
lookingUp: boolean;
|
|
lookup: () => Promise<void>;
|
|
submit: () => void;
|
|
}
|
|
|
|
function factory() {
|
|
const w = mount(SalesProspectCreateDialog, {
|
|
props: { modelValue: true },
|
|
global: { plugins: [vuetify] },
|
|
});
|
|
return { w, vm: w.vm as unknown as CreateVm };
|
|
}
|
|
|
|
describe('SalesProspectCreateDialog', () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it('без названия не эмитит create', () => {
|
|
const { w, vm } = factory();
|
|
vm.inn = '5501234560';
|
|
vm.submit();
|
|
expect(w.emitted('create')).toBeFalsy();
|
|
});
|
|
|
|
it('без ИНН не эмитит create и объясняет почему', () => {
|
|
const { w, vm } = factory();
|
|
vm.firmName = 'Без ИНН';
|
|
vm.submit();
|
|
expect(w.emitted('create')).toBeFalsy();
|
|
expect(vm.error).toContain('ИНН');
|
|
});
|
|
|
|
it('кривой ИНН не пускаем дальше формы', () => {
|
|
const { w, vm } = factory();
|
|
vm.firmName = 'Опечатка';
|
|
vm.inn = '5501234567';
|
|
vm.submit();
|
|
expect(w.emitted('create')).toBeFalsy();
|
|
});
|
|
|
|
it('эмитит create с брендом, юрлицом и контактными лицами', () => {
|
|
const { w, vm } = factory();
|
|
vm.firmName = 'Стоматология «Улыбка»';
|
|
vm.legalName = 'ООО «Улыбка Плюс»';
|
|
vm.inn = '5501234560';
|
|
vm.city = 'Омск';
|
|
vm.contacts = [
|
|
{ name: 'Иванов Иван', position: 'директор', phones: ['+79130000001', '+73812000002'] },
|
|
];
|
|
vm.submit();
|
|
|
|
expect(w.emitted('create')?.[0]?.[0]).toMatchObject({
|
|
firm_name: 'Стоматология «Улыбка»',
|
|
legal_name: 'ООО «Улыбка Плюс»',
|
|
inn: '5501234560',
|
|
city: 'Омск',
|
|
contacts: [
|
|
{ name: 'Иванов Иван', position: 'директор', phones: ['+79130000001', '+73812000002'] },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('пустые контакты и телефоны не уходят на сервер', () => {
|
|
const { w, vm } = factory();
|
|
vm.firmName = 'Полупустые';
|
|
vm.inn = '5501234560';
|
|
vm.contacts = [
|
|
{ name: ' ', position: '', phones: ['', ' '] },
|
|
{ name: 'Сидоров', position: '', phones: ['+79130000009', ''] },
|
|
];
|
|
vm.submit();
|
|
|
|
const payload = w.emitted('create')?.[0]?.[0] as Record<string, unknown>;
|
|
expect(payload.contacts).toEqual([{ name: 'Сидоров', position: null, phones: ['+79130000009'] }]);
|
|
});
|
|
|
|
it('поиск по ИНН подставляет юрлицо и город', async () => {
|
|
(lookupProspectInn as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
found: true,
|
|
legal_name: 'ООО «Улыбка Плюс»',
|
|
city: 'Омск',
|
|
});
|
|
const { vm } = factory();
|
|
vm.inn = '5501234560';
|
|
await vm.lookup();
|
|
await flushPromises();
|
|
|
|
expect(lookupProspectInn).toHaveBeenCalledWith('5501234560');
|
|
expect(vm.legalName).toBe('ООО «Улыбка Плюс»');
|
|
expect(vm.city).toBe('Омск');
|
|
});
|
|
|
|
it('поиск не затирает город, если менеджер уже вписал свой', async () => {
|
|
(lookupProspectInn as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
found: true,
|
|
legal_name: 'ООО «Улыбка Плюс»',
|
|
city: 'Омск',
|
|
});
|
|
const { vm } = factory();
|
|
vm.inn = '5501234560';
|
|
vm.city = 'Красноярск';
|
|
await vm.lookup();
|
|
await flushPromises();
|
|
|
|
expect(vm.city).toBe('Красноярск');
|
|
});
|
|
|
|
it('ДаData не нашла → говорим об этом и не мешаем заполнить руками', async () => {
|
|
(lookupProspectInn as ReturnType<typeof vi.fn>).mockResolvedValue({ found: false });
|
|
const { vm } = factory();
|
|
vm.inn = '5501234560';
|
|
await vm.lookup();
|
|
await flushPromises();
|
|
|
|
expect(vm.error).not.toBe('');
|
|
expect(vm.legalName).toBe('');
|
|
expect(vm.lookingUp).toBe(false);
|
|
});
|
|
|
|
it('ДаData упала → форма остаётся рабочей', async () => {
|
|
(lookupProspectInn as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('network'));
|
|
const { vm } = factory();
|
|
vm.inn = '5501234560';
|
|
await vm.lookup();
|
|
await flushPromises();
|
|
|
|
expect(vm.lookingUp).toBe(false);
|
|
});
|
|
});
|