diff --git a/app/resources/js/views/KanbanView.vue b/app/resources/js/views/KanbanView.vue index 6a3d1022..cf0e93f6 100644 --- a/app/resources/js/views/KanbanView.vue +++ b/app/resources/js/views/KanbanView.vue @@ -5,13 +5,16 @@ * Источник дизайна: liderra_v8_handoff/concepts/v8_kanban.html. * DnD реализован через vuedraggable@4 (обёртка SortableJS) — карточки можно * перетаскивать между колонками. При drop: - * - событие 'added' в целевой колонке → меняем `statusSlug` сделки. + * - событие 'added' в целевой колонке → optimistic update statusSlug + + * POST /api/deals/transition (через dealsApi). На failure — revert: + * карточка возвращается в исходную колонку + toast «Не удалось переместить». + * Без auth.user.tenant_id — local-only mode (API не зовётся). * - событие 'removed' в исходной колонке → ничего не делаем (обработано в added). * - событие 'moved' внутри одной колонки → только смена порядка (statusSlug * не меняется; на API будущем — PATCH /api/deals/{id} {sort_order}). * * Не входит в этот коммит: - * - PATCH /api/deals/{id} {status_slug} при drop — backend. + * - PATCH /api/deals/{id} {sort_order} при moved (intra-column reorder) — backend. * - Filters (Проект/Менеджер) — общий filter-bar с DealsView. * - DealDetailDrawer на click по карточке (event @open-deal). */ diff --git a/app/tests/Frontend/KanbanView.spec.ts b/app/tests/Frontend/KanbanView.spec.ts index 3c2706c1..f67a6a90 100644 --- a/app/tests/Frontend/KanbanView.spec.ts +++ b/app/tests/Frontend/KanbanView.spec.ts @@ -160,15 +160,23 @@ describe('KanbanView DnD persist (Sprint 1 C4)', () => { await new Promise((r) => setTimeout(r, 30)); const deal = { id: 43, statusSlug: 'new' as const, name: 'Y', phone: '+79161234567', project: 'p', manager: { name: 'M', initials: 'M' }, cost: 100, receivedMinutesAgo: 5 }; + // Имитируем vuedraggable mutation: карточка уже в target column до вызова onColumnChange. // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (wrapper.vm as any).onColumnChange('hot', { added: { element: deal, newIndex: 0 } }); + const vm = wrapper.vm as any; + if (!vm.dealsByStatus.hot) vm.dealsByStatus.hot = []; + vm.dealsByStatus.hot.push(deal); - // После failure — statusSlug откатывается на оригинал + await vm.onColumnChange('hot', { added: { element: deal, newIndex: 0 } }); + + // statusSlug rolled back expect(deal.statusSlug).toBe('new'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((wrapper.vm as any).transitionToastOpen).toBe(true); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((wrapper.vm as any).transitionToastText).toContain('Не удалось'); + // Card removed from target column (array-revert branch coverage) + expect(vm.dealsByStatus.hot.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 + expect(vm.transitionToastOpen).toBe(true); + expect(vm.transitionToastText).toContain('Не удалось'); }); it('onColumnChange skips API call if no auth.user.tenant_id', async () => {