feat(deals): Kanban — 5-column funnel (comment + test sync)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Канбан — альтернативный вид сделок (по статусам). 14 колонок (lead_statuses).
|
||||
* Канбан — альтернативный вид сделок (по статусам). 5 колонок (lead_statuses).
|
||||
*
|
||||
* Источник дизайна: liderra_v8_handoff/concepts/v8_kanban.html.
|
||||
* DnD реализован через vuedraggable@4 (обёртка SortableJS) — карточки можно
|
||||
|
||||
@@ -28,11 +28,11 @@ describe('KanbanView.vue', () => {
|
||||
expect(wrapper.find('h1').text()).toBe('Канбан');
|
||||
});
|
||||
|
||||
it('рендерит ровно 14 KanbanColumn (по числу lead_statuses)', () => {
|
||||
it('рендерит ровно 5 KanbanColumn (по числу lead_statuses)', () => {
|
||||
const wrapper = factory();
|
||||
const cols = wrapper.findAllComponents({ name: 'KanbanColumn' });
|
||||
expect(cols).toHaveLength(LEAD_STATUSES.length);
|
||||
expect(cols).toHaveLength(14);
|
||||
expect(cols).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('каждая колонка получает соответствующий статус', () => {
|
||||
@@ -46,7 +46,7 @@ describe('KanbanView.vue', () => {
|
||||
it('содержит page-stats с числом статусов и сделок', () => {
|
||||
const wrapper = factory();
|
||||
const text = wrapper.text();
|
||||
expect(text).toContain('14');
|
||||
expect(text).toContain('5');
|
||||
expect(text).toContain('статусов');
|
||||
expect(text).toContain('сделок');
|
||||
});
|
||||
@@ -110,17 +110,17 @@ describe('KanbanView.vue', () => {
|
||||
const cols = wrapper.findAllComponents({ name: 'KanbanColumn' });
|
||||
// Берём сделку из первой колонки (new) и эмулируем «added» в paid-колонке.
|
||||
const newCol = cols[0]; // new — sortOrder=1
|
||||
const paidCol = cols.find((c) => c.props('status').slug === 'paid')!;
|
||||
const wonCol = cols.find((c) => c.props('status').slug === 'won')!;
|
||||
const dealToMove = (newCol.props('deals') as { id: number; statusSlug: string }[])[0];
|
||||
|
||||
// Эмуляция события vuedraggable@change → KanbanView.onColumnChange.
|
||||
await paidCol.vm.$emit('change', {
|
||||
await wonCol.vm.$emit('change', {
|
||||
added: { element: dealToMove, newIndex: 0 },
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// statusSlug сделки должен переключиться на 'paid'.
|
||||
expect(dealToMove.statusSlug).toBe('paid');
|
||||
// statusSlug сделки должен переключиться на 'won'.
|
||||
expect(dealToMove.statusSlug).toBe('won');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -160,7 +160,7 @@ describe('KanbanView DnD persist (Sprint 1 C4)', () => {
|
||||
const transitionSpy = vi.spyOn(dealsApi, 'transitionDeals').mockResolvedValue({
|
||||
updated: 1,
|
||||
requested: 1,
|
||||
status: 'hot',
|
||||
status: 'in_progress',
|
||||
});
|
||||
const wrapper = mount(KanbanView, {
|
||||
global: {
|
||||
@@ -174,14 +174,14 @@ describe('KanbanView DnD persist (Sprint 1 C4)', () => {
|
||||
|
||||
const deal = { id: 42, statusSlug: 'new' as const, name: 'X', phone: '+79161234567', project: 'p', manager: { name: 'M', initials: 'M' }, cost: 100, receivedMinutesAgo: 5 };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await (wrapper.vm as any).onColumnChange('hot', { added: { element: deal, newIndex: 0 } });
|
||||
await (wrapper.vm as any).onColumnChange('in_progress', { added: { element: deal, newIndex: 0 } });
|
||||
|
||||
expect(transitionSpy).toHaveBeenCalledWith({
|
||||
tenant_id: 7,
|
||||
ids: [42],
|
||||
status: 'hot',
|
||||
status: 'in_progress',
|
||||
});
|
||||
expect(deal.statusSlug).toBe('hot');
|
||||
expect(deal.statusSlug).toBe('in_progress');
|
||||
});
|
||||
|
||||
it('onColumnChange reverts statusSlug + opens toast when API rejects', async () => {
|
||||
@@ -200,15 +200,15 @@ describe('KanbanView DnD persist (Sprint 1 C4)', () => {
|
||||
// Имитируем vuedraggable mutation: карточка уже в target column до вызова onColumnChange.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const vm = wrapper.vm as any;
|
||||
if (!vm.dealsByStatus.hot) vm.dealsByStatus.hot = [];
|
||||
vm.dealsByStatus.hot.push(deal);
|
||||
if (!vm.dealsByStatus.in_progress) vm.dealsByStatus.in_progress = [];
|
||||
vm.dealsByStatus.in_progress.push(deal);
|
||||
|
||||
await vm.onColumnChange('hot', { added: { element: deal, newIndex: 0 } });
|
||||
await vm.onColumnChange('in_progress', { added: { element: deal, newIndex: 0 } });
|
||||
|
||||
// statusSlug rolled back
|
||||
expect(deal.statusSlug).toBe('new');
|
||||
// Card removed from target column (array-revert branch coverage)
|
||||
expect(vm.dealsByStatus.hot.findIndex((d: { id: number }) => d.id === 43)).toBe(-1);
|
||||
expect(vm.dealsByStatus.in_progress.findIndex((d: { id: number }) => d.id === 43)).toBe(-1);
|
||||
// Card restored to source column
|
||||
expect(vm.dealsByStatus.new.findIndex((d: { id: number }) => d.id === 43)).toBeGreaterThanOrEqual(0);
|
||||
// Toast shown
|
||||
@@ -218,7 +218,7 @@ describe('KanbanView DnD persist (Sprint 1 C4)', () => {
|
||||
|
||||
it('onColumnChange skips API call if no auth.user.tenant_id', async () => {
|
||||
const transitionSpy = vi.spyOn(dealsApi, 'transitionDeals').mockResolvedValue({
|
||||
updated: 1, requested: 1, status: 'hot',
|
||||
updated: 1, requested: 1, status: 'in_progress',
|
||||
});
|
||||
const wrapper = mount(KanbanView, {
|
||||
global: {
|
||||
@@ -232,10 +232,10 @@ describe('KanbanView DnD persist (Sprint 1 C4)', () => {
|
||||
|
||||
const deal = { id: 44, statusSlug: 'new' as const, name: 'Z', phone: '+79161234567', project: 'p', manager: { name: 'M', initials: 'M' }, cost: 100, receivedMinutesAgo: 5 };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await (wrapper.vm as any).onColumnChange('hot', { added: { element: deal, newIndex: 0 } });
|
||||
await (wrapper.vm as any).onColumnChange('in_progress', { added: { element: deal, newIndex: 0 } });
|
||||
|
||||
// Без auth — только optimistic local change, API не зовётся
|
||||
expect(transitionSpy).not.toHaveBeenCalled();
|
||||
expect(deal.statusSlug).toBe('hot');
|
||||
expect(deal.statusSlug).toBe('in_progress');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user