From 3bc4325b78814feb24cd084f0d9638a67366faa3 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: Sun, 28 Jun 2026 15:41:07 +0300 Subject: [PATCH] =?UTF-8?q?feat(=D0=B0=D0=B2=D1=82=D0=BE=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=B1=D0=BE=D1=80):=20=D1=84=D1=80=D0=BE=D0=BD=D1=82=20?= =?UTF-8?q?=E2=80=94=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D0=BA=D1=83=D1=80=D0=B5=D0=BD=D1=82=D0=B0=20(=D0=B2=D1=8B?= =?UTF-8?q?=D0=B1=D0=BE=D1=80,=20=D1=80=D1=83=D1=87=D0=BD=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../js/views/autopodbor/AutopodborView.vue | 3 + .../views/autopodbor/screens/DetailScreen.vue | 585 ++++++++++++++++++ .../Frontend/AutopodborDetailScreen.spec.ts | 75 +++ 3 files changed, 663 insertions(+) create mode 100644 app/resources/js/views/autopodbor/screens/DetailScreen.vue create mode 100644 app/tests/Frontend/AutopodborDetailScreen.spec.ts diff --git a/app/resources/js/views/autopodbor/AutopodborView.vue b/app/resources/js/views/autopodbor/AutopodborView.vue index 85e6e330..19224eba 100644 --- a/app/resources/js/views/autopodbor/AutopodborView.vue +++ b/app/resources/js/views/autopodbor/AutopodborView.vue @@ -6,6 +6,7 @@ import AutoFormScreen from './screens/AutoFormScreen.vue'; import ManualFormScreen from './screens/ManualFormScreen.vue'; import LoadingScreen from './screens/LoadingScreen.vue'; import ListScreen from './screens/ListScreen.vue'; +import DetailScreen from './screens/DetailScreen.vue'; type ScreenName = | 'entry' @@ -28,6 +29,7 @@ const ctx = reactive({ selectedSourceIds: [] as number[], loadMsg: '', loadSub: '', + editProjectId: null as number | null, }); function go(name: ScreenName) { @@ -43,6 +45,7 @@ const screens: Partial> = { manualform: ManualFormScreen, loading: LoadingScreen, list: ListScreen, + detail: DetailScreen, }; onMounted(() => { diff --git a/app/resources/js/views/autopodbor/screens/DetailScreen.vue b/app/resources/js/views/autopodbor/screens/DetailScreen.vue new file mode 100644 index 00000000..1de9c9bc --- /dev/null +++ b/app/resources/js/views/autopodbor/screens/DetailScreen.vue @@ -0,0 +1,585 @@ + + + + + diff --git a/app/tests/Frontend/AutopodborDetailScreen.spec.ts b/app/tests/Frontend/AutopodborDetailScreen.spec.ts new file mode 100644 index 00000000..580af3aa --- /dev/null +++ b/app/tests/Frontend/AutopodborDetailScreen.spec.ts @@ -0,0 +1,75 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import { createPinia, setActivePinia } from 'pinia'; +import { createVuetify } from 'vuetify'; +import { reactive, ref } from 'vue'; + +vi.mock('../../resources/js/api/autopodbor'); +import DetailScreen from '../../resources/js/views/autopodbor/screens/DetailScreen.vue'; +import { useAutopodborStore } from '../../resources/js/stores/autopodborStore'; + +const vuetify = createVuetify(); +function makeNav(competitorId = 3) { + return { go: vi.fn(), ctx: reactive({ runId: null, competitorId, selectedSourceIds: [] as number[], loadMsg: '', loadSub: '', editProjectId: null as number|null }), screen: ref('detail') }; +} +function mountDetail(nav: any) { + return mount(DetailScreen, { global: { plugins: [vuetify], provide: { autopodborNav: nav } } }); +} + +function seed(store: any) { + store.competitor = { id: 3, name: 'Окна Комфорт', is_federal: false, relevance_pct: 100, origin: 'auto', site_url: 'okna.ru', directory_urls: [], description: 'd', studied_at: '2026-06-28', study_run_id: 9, search_run_id: 5 }; + store.sources = [ + { id: 11, competitor_id: 3, signal_type: 'site', identifier: 'okna-komfort-kzn.ru', phone_kind: null, provenance_url: null, provenance_label: '2ГИС', created_project_id: null, existing_project_id: null }, + { id: 12, competitor_id: 3, signal_type: 'call', identifier: '78432001122', phone_kind: 'real', provenance_url: null, provenance_label: '2ГИС', created_project_id: null, existing_project_id: null }, + { id: 13, competitor_id: 3, signal_type: 'call', identifier: '78003507700', phone_kind: 'substitute', provenance_url: null, provenance_label: 'футер', created_project_id: 99, existing_project_id: 99 }, + ]; +} + +describe('DetailScreen', () => { + beforeEach(() => { setActivePinia(createPinia()); vi.clearAllMocks(); }); + + it('грузит конкурента и показывает источники', async () => { + const store = useAutopodborStore(); + vi.spyOn(store, 'loadCompetitor').mockImplementation(async () => seed(store)); + const nav = makeNav(3); + const w = mountDetail(nav); + await new Promise(r => setTimeout(r, 0)); + expect(store.loadCompetitor).toHaveBeenCalledWith(3); + expect(w.text()).toContain('okna-komfort-kzn.ru'); + expect(w.text()).toContain('проект создан'); // источник 13 с existing_project_id + }); + + it('по умолчанию выбраны источники без проекта (11 и 12, не 13)', async () => { + const store = useAutopodborStore(); + vi.spyOn(store, 'loadCompetitor').mockImplementation(async () => seed(store)); + const nav = makeNav(3); + mountDetail(nav); + await new Promise(r => setTimeout(r, 0)); + expect(nav.ctx.selectedSourceIds).toContain(11); + expect(nav.ctx.selectedSourceIds).toContain(12); + expect(nav.ctx.selectedSourceIds).not.toContain(13); + }); + + it('«Создать проекты» ведёт на create', async () => { + const store = useAutopodborStore(); + vi.spyOn(store, 'loadCompetitor').mockImplementation(async () => seed(store)); + const nav = makeNav(3); + const w = mountDetail(nav); + await new Promise(r => setTimeout(r, 0)); + const btn = w.findAll('button').find(b => b.text().includes('Создать проекты')); + await btn!.trigger('click'); + expect(nav.go).toHaveBeenCalledWith('create'); + }); + + it('«Изменить проект» у созданного источника ведёт на editproject', async () => { + const store = useAutopodborStore(); + vi.spyOn(store, 'loadCompetitor').mockImplementation(async () => seed(store)); + const nav = makeNav(3); + const w = mountDetail(nav); + await new Promise(r => setTimeout(r, 0)); + const btn = w.findAll('button').find(b => b.text().includes('Изменить проект')); + await btn!.trigger('click'); + expect(nav.ctx.editProjectId).toBe(99); + expect(nav.go).toHaveBeenCalledWith('editproject'); + }); +});