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>
@
64 lines
2.7 KiB
TypeScript
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);
|
|
});
|
|
});
|