Files
portal/app/tests/Frontend/SalesProspectsBoardView.spec.ts
T
Дмитрий 277800149f @
fix(sales): строгое разделение двух ролей начальника — экраны «мои» больше не показывают отдел

Начальник продаёт сам и руководит отделом. Правило «начальник видит всё»
применялось к ЧЕЛОВЕКУ, а не к экрану, поэтому разделы меню врали:

- «Потенциальные клиенты» показывали все карточки отдела — точную копию
  «Воронки отдела» (это владелец и заметил);
- «Мои клиенты» и «Сводка» — всех клиентов отдела;
- «Привязать клиента» — очередь заявок отдела, причём в ЧУЖОМ формате
  ({pending, history} вместо {data}), форма получала не те данные.

Новое правило: роли разделяются по ЭКРАНУ. Любой запрос по умолчанию отдаёт
только личное — включая начальника. Весь отдел выдаётся только по явному
?scope=department, и просят его только экраны раздела НАЧАЛЬНИК. Менеджеру
параметр ничего не даёт: проверка по роли, не по параметру.

ownedTenantIds теперь ВСЕГДА личные привязки (тип сузился с ?array до array),
добавлен visibleTenantIds для области видимости запроса.

Правом начальника осталось открыть ЛЮБУЮ карточку — кандидата и клиента:
иначе из «Воронки отдела» не открылась бы карточка чужого менеджера.
Ограничены только списки, не доступ к записи.

Следствие: у начальника сейчас 0 своих кандидатов и клиентов, поэтому его
личные экраны станут пустыми — это правильно, а не поломка.

Pest 272/272, Vitest зелёный, vue-tsc чист, Larastan без своих ошибок.
Спека — §23.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-18 15:59:56 +03:00

64 lines
2.7 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', () => ({
listProspects: vi.fn().mockResolvedValue({ prospects: [], by_stage: {}, stages: [] }),
listSalesManagers: vi.fn().mockResolvedValue([
{ id: 7, name: 'Иванов', email: 'i@s', role: 'manager', is_active: true, clients_count: 0, paid_all_time_rub: 0 },
]),
updateProspect: vi.fn(),
extractSalesErrorMessage: () => 'err',
}));
import SalesProspectsBoardView from '../../resources/js/views/sales/SalesProspectsBoardView.vue';
import { listProspects } from '../../resources/js/api/sales';
const vuetify = createVuetify();
interface BoardVm {
managerId: number | null;
load: () => Promise<void>;
managerItems: { id: number | null; name: string }[];
}
describe('SalesProspectsBoardView', () => {
beforeEach(() => vi.clearAllMocks());
it('при выборе менеджера перезагружает с его id', async () => {
const w = mount(SalesProspectsBoardView, { global: { plugins: [vuetify] } });
await flushPromises();
// 'department' — воронка отдела просит весь отдел явно (§23).
expect(listProspects).toHaveBeenCalledWith(undefined, undefined, 'department');
(w.vm as unknown as BoardVm).managerId = 7;
await (w.vm as unknown as BoardVm).load();
expect(listProspects).toHaveBeenLastCalledWith(7, undefined, 'department');
});
it('фильтр происхождения уходит в запрос вторым параметром', async () => {
const w = mount(SalesProspectsBoardView, { global: { plugins: [vuetify] } });
await flushPromises();
(w.vm as unknown as BoardVm & { source: string | null }).source = 'manager';
await (w.vm as unknown as BoardVm).load();
expect(listProspects).toHaveBeenLastCalledWith(undefined, 'manager', 'department');
});
it('в фильтре у менеджера показывается число его карточек', async () => {
(listProspects as ReturnType<typeof vi.fn>).mockResolvedValue({
prospects: [],
by_stage: {},
stages: [],
manager_counts: { '7': 3 },
});
const w = mount(SalesProspectsBoardView, { global: { plugins: [vuetify] } });
await flushPromises();
const items = (w.vm as unknown as BoardVm).managerItems;
expect(items[0]).toMatchObject({ id: null, name: 'Все менеджеры' });
expect(items.some((i) => i.name === 'Иванов (3)')).toBe(true);
});
});