66204a045c
Task 2.3b портала отдела продаж — два Vue-экрана поверх готовых эндпоинтов /api/sales/attachments: - SalesAttachView (менеджер, #page-attach): форма заявки по логину (e-mail/ИНН), живой баннер результата (отправлено/не найден/ошибка), таблица «Мои заявки». - SalesRequestsView (начальник, #page-requests): очередь на решение с проверкой «свободен / уже за: …», кнопки Подтвердить/Переназначить /Отклонить (переназначение = тот же approve на бэке), история, счётчик. - api/sales.ts: submitAttachment / listMyAttachments / listAttachmentQueue / decideAttachment + типы. - Роуты /sales/attach и /sales/requests переключены со stub на реальные экраны. Vitest: SalesAttach 5 + SalesRequests 7 = 12 зелёных; полный фронт-набор не сломан. ESLint чист. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
175 lines
6.9 KiB
TypeScript
175 lines
6.9 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 SalesRequestsView from '../../resources/js/views/sales/SalesRequestsView.vue';
|
|
|
|
// ─── mock sales api ───────────────────────────────────────────────────────────
|
|
|
|
vi.mock('../../resources/js/api/sales', () => ({
|
|
listAttachmentQueue: vi.fn().mockResolvedValue({
|
|
pending: [
|
|
{
|
|
id: 1,
|
|
login_input: 'info@potolki-spb.ru',
|
|
status: 'pending',
|
|
status_label: 'На рассмотрении',
|
|
tenant_id: 601,
|
|
organization_name: 'Натяжные потолки СПб',
|
|
manager_name: 'Алексей Смирнов',
|
|
hint: 'свободен',
|
|
comment: null,
|
|
created_at: '2026-06-28T10:14:00Z',
|
|
decided_at: null,
|
|
},
|
|
{
|
|
id: 2,
|
|
login_input: 'info@dveri-prem.ru',
|
|
status: 'pending',
|
|
status_label: 'На рассмотрении',
|
|
tenant_id: 602,
|
|
organization_name: 'Двери Премиум',
|
|
manager_name: 'Мария Котова',
|
|
hint: 'уже за: А. Смирнов',
|
|
comment: null,
|
|
created_at: '2026-06-28T09:02:00Z',
|
|
decided_at: null,
|
|
},
|
|
{
|
|
id: 3,
|
|
login_input: 'info@unknown.ru',
|
|
status: 'pending',
|
|
status_label: 'На рассмотрении',
|
|
tenant_id: null,
|
|
organization_name: null,
|
|
manager_name: 'Игорь Лебедев',
|
|
hint: null,
|
|
comment: null,
|
|
created_at: '2026-06-28T08:00:00Z',
|
|
decided_at: null,
|
|
},
|
|
],
|
|
history: [
|
|
{
|
|
id: 90,
|
|
login_input: 'sale@kuhni-ekb.ru',
|
|
status: 'approved',
|
|
status_label: 'Одобрено',
|
|
tenant_id: 501,
|
|
organization_name: 'Кухни на заказ Екб',
|
|
manager_name: 'Алексей Смирнов',
|
|
comment: null,
|
|
created_at: '2026-06-27T16:10:00Z',
|
|
decided_at: '2026-06-27T16:12:00Z',
|
|
},
|
|
],
|
|
}),
|
|
decideAttachment: vi.fn().mockResolvedValue({
|
|
id: 1,
|
|
login_input: 'info@potolki-spb.ru',
|
|
status: 'approved',
|
|
status_label: 'Одобрено',
|
|
tenant_id: 601,
|
|
comment: null,
|
|
}),
|
|
extractSalesErrorMessage: vi.fn((_e: unknown, fallback = 'Произошла ошибка. Попробуйте позже.') => fallback),
|
|
}));
|
|
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
function makeRouter() {
|
|
const router = createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [{ path: '/sales/requests', name: 'sales-requests', component: SalesRequestsView }],
|
|
});
|
|
return router.push('/sales/requests').then(() => router);
|
|
}
|
|
|
|
async function mountView() {
|
|
const router = await makeRouter();
|
|
const wrapper = mount(SalesRequestsView, {
|
|
global: { plugins: [createVuetify(), createPinia(), router] },
|
|
});
|
|
await flushPromises();
|
|
return wrapper;
|
|
}
|
|
|
|
describe('SalesRequestsView', () => {
|
|
it('заголовок + счётчик = число pending', async () => {
|
|
const wrapper = await mountView();
|
|
const text = wrapper.text();
|
|
expect(text).toContain('Заявки на привязку');
|
|
expect(text).toContain('3 ждут решения');
|
|
|
|
const api = await import('../../resources/js/api/sales');
|
|
expect(api.listAttachmentQueue).toHaveBeenCalled();
|
|
});
|
|
|
|
it('очередь рендерит chip по hint (свободен / уже за / не найден)', async () => {
|
|
const wrapper = await mountView();
|
|
const text = wrapper.text();
|
|
expect(text).toContain('Алексей Смирнов');
|
|
expect(text).toContain('info@potolki-spb.ru');
|
|
expect(text).toContain('НАЙДЕН · свободен');
|
|
expect(text).toContain('уже за: А. Смирнов');
|
|
expect(text).toContain('не найден');
|
|
});
|
|
|
|
it('история рендерит строки', async () => {
|
|
const wrapper = await mountView();
|
|
const text = wrapper.text();
|
|
expect(text).toContain('Кухни на заказ Екб');
|
|
expect(text).toContain('Одобрено');
|
|
});
|
|
|
|
it('клик «Подтвердить» на свободном → decideAttachment(id, approve)', async () => {
|
|
const wrapper = await mountView();
|
|
const api = await import('../../resources/js/api/sales');
|
|
|
|
const btn = wrapper.find('[data-testid="approve-1"]');
|
|
expect(btn.exists()).toBe(true);
|
|
await btn.trigger('click');
|
|
await flushPromises();
|
|
|
|
expect(api.decideAttachment).toHaveBeenCalledWith(1, 'approve', undefined);
|
|
});
|
|
|
|
it('клик «Переназначить» на конфликте → decideAttachment(id, approve)', async () => {
|
|
const wrapper = await mountView();
|
|
const api = await import('../../resources/js/api/sales');
|
|
|
|
const btn = wrapper.find('[data-testid="approve-2"]');
|
|
expect(btn.exists()).toBe(true);
|
|
await btn.trigger('click');
|
|
await flushPromises();
|
|
|
|
expect(api.decideAttachment).toHaveBeenCalledWith(2, 'approve', undefined);
|
|
});
|
|
|
|
it('клик «Отклонить» → decideAttachment(id, reject)', async () => {
|
|
const wrapper = await mountView();
|
|
const api = await import('../../resources/js/api/sales');
|
|
|
|
const btn = wrapper.find('[data-testid="reject-1"]');
|
|
expect(btn.exists()).toBe(true);
|
|
await btn.trigger('click');
|
|
await flushPromises();
|
|
|
|
expect(api.decideAttachment).toHaveBeenCalledWith(1, 'reject', undefined);
|
|
});
|
|
|
|
it('после решения список перезагружается', async () => {
|
|
const wrapper = await mountView();
|
|
const api = await import('../../resources/js/api/sales');
|
|
(api.listAttachmentQueue as ReturnType<typeof vi.fn>).mockClear();
|
|
|
|
await wrapper.find('[data-testid="approve-1"]').trigger('click');
|
|
await flushPromises();
|
|
|
|
expect(api.listAttachmentQueue).toHaveBeenCalled();
|
|
});
|
|
});
|