From c09c52ea76673c6dbf2b2f10cb3aa7f1aebb1824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 15 May 2026 08:43:49 +0300 Subject: [PATCH] =?UTF-8?q?refactor(deals):=20C2=20review=20fixes=20?= =?UTF-8?q?=E2=80=94=20watcher-driven=20draft=20+=20named=20toggles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code-review fixes для commit 4e77947 (C2 FilterChip popovers): I-4 (latent interaction bug): Удалена двойная open-path в FilterChip activator. v-menu сам управляет projectMenuOpen/managerMenuOpen через activatorProps. Draft-state копируется при menu open → true через watch(menuOpen, ...). Раньше: - Activator click: menuOpen=true - @click on FilterChip: onRedesignFilterClick → menuOpen=true (duplicate) - Re-click для close: activator toggles false → onRedesignFilterClick forces true back → menu не закрывается. I-2 (inline toggle extract): Multi-line ternary @click заменён на named methods toggleProjectDraft(proj) / toggleManagerDraft(name). Консистентно с existing clearProjectDraft / clearManagerDraft. Также unit-testable независимо от template. onRedesignFilterClick остаётся для Status chip read-only behavior (P2 backlog Sprint 5). defineExpose обновлён: убран onRedesignFilterClick, добавлены toggleProjectDraft/toggleManagerDraft/clearProjectDraft/ clearManagerDraft (для будущих spec'ов). Vitest 3/3 C2-specs обновлены на прямой trigger projectMenuOpen=true + $nextTick (watcher seeds draft). Регрессий 0. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/resources/js/views/DealsView.vue | 59 ++++++++++++-------- app/tests/Frontend/DealsViewRedesign.spec.ts | 12 +++- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/app/resources/js/views/DealsView.vue b/app/resources/js/views/DealsView.vue index 5d26d307..1e06b767 100644 --- a/app/resources/js/views/DealsView.vue +++ b/app/resources/js/views/DealsView.vue @@ -13,7 +13,7 @@ * * Источник статусов — composables/leadStatuses.ts (snapshot из db/schema.sql:2130). */ -import { computed, defineAsyncComponent, onMounted, reactive, ref } from 'vue'; +import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'; import { DEALS_TABS, MOCK_DEALS, type MockDeal } from '../composables/mockDeals'; import { mapApiDeal } from '../composables/dealsApiMapper'; import { usePolling } from '../composables/usePolling'; @@ -38,22 +38,30 @@ import { buildCsvString, triggerBlobDownload, triggerCsvDownload } from '../comp const { rowHeight } = useDensity(); // Sprint 1 C2: popovers для Проект/Менеджер chip'ов. Draft-state накапливает -// выбор в v-menu, при «Применить» переносится в filterProjects/filterManagers. -// Status chip остаётся read-only (P2 backlog в Sprint 5). +// выбор внутри v-menu; копируется из filterProjects/filterManagers при открытии +// (watch на menu open=true); переносится обратно в filterProjects/Managers на +// «Применить» button. Status chip — read-only (P2 backlog Sprint 5). const projectMenuOpen = ref(false); const managerMenuOpen = ref(false); const projectMenuDraft = ref([]); const managerMenuDraft = ref([]); +// При открытии меню — копируем текущий filter в draft (snapshot-on-open). +// При закрытии без apply — draft остаётся, но не влияет на filterProjects +// (apply нужен явно). +watch(projectMenuOpen, (isOpen) => { + if (isOpen) projectMenuDraft.value = [...filterProjects.value]; +}); +watch(managerMenuOpen, (isOpen) => { + if (isOpen) managerMenuDraft.value = [...filterManagers.value]; +}); + function onRedesignFilterClick(name: string): void { - if (name === 'Проект') { - projectMenuDraft.value = [...filterProjects.value]; - projectMenuOpen.value = true; - } else if (name === 'Менеджер') { - managerMenuDraft.value = [...filterManagers.value]; - managerMenuOpen.value = true; + // Status chip — read-only summary (P2 backlog Sprint 5). + // Project/Manager managed by v-menu activator (no manual click handler needed). + if (name === 'Статус') { + // no-op — placeholder для будущей реализации } - // 'Статус' — read-only summary, popover откроется в Sprint 5 (P2). } function applyProjectFilter(): void { @@ -74,6 +82,18 @@ function clearManagerDraft(): void { managerMenuDraft.value = []; } +function toggleProjectDraft(proj: string): void { + projectMenuDraft.value = projectMenuDraft.value.includes(proj) + ? projectMenuDraft.value.filter((p) => p !== proj) + : [...projectMenuDraft.value, proj]; +} + +function toggleManagerDraft(name: string): void { + managerMenuDraft.value = managerMenuDraft.value.includes(name) + ? managerMenuDraft.value.filter((m) => m !== name) + : [...managerMenuDraft.value, name]; +} + const auth = useAuthStore(); const leadStatusesStore = useLeadStatusesStore(); @@ -386,7 +406,10 @@ defineExpose({ managerMenuDraft, applyProjectFilter, applyManagerFilter, - onRedesignFilterClick, + clearProjectDraft, + clearManagerDraft, + toggleProjectDraft, + toggleManagerDraft, }); const leadStatuses = computed(() => leadStatusesStore.statuses); @@ -518,7 +541,6 @@ const waitingPay = computed(() => dealsState.filter((d) => d.statusSlug === 'wai label="Проект" :count="filterProjects.length" :active="filterProjects.length > 0" - @click="onRedesignFilterClick('Проект')" /> @@ -532,11 +554,7 @@ const waitingPay = computed(() => dealsState.filter((d) => d.statusSlug === 'wai v-for="proj in availableProjects" :key="proj" class="py-1" - @click=" - projectMenuDraft.includes(proj) - ? (projectMenuDraft = projectMenuDraft.filter((p) => p !== proj)) - : (projectMenuDraft = [...projectMenuDraft, proj]) - " + @click="toggleProjectDraft(proj)" > @@ -583,11 +600,7 @@ const waitingPay = computed(() => dealsState.filter((d) => d.statusSlug === 'wai v-for="mgr in availableManagers" :key="mgr.name" class="py-1" - @click=" - managerMenuDraft.includes(mgr.name) - ? (managerMenuDraft = managerMenuDraft.filter((m) => m !== mgr.name)) - : (managerMenuDraft = [...managerMenuDraft, mgr.name]) - " + @click="toggleManagerDraft(mgr.name)" >