diff --git a/app/resources/js/views/admin/AdminSupplierIntegrationView.vue b/app/resources/js/views/admin/AdminSupplierIntegrationView.vue index e829384f..7db4124d 100644 --- a/app/resources/js/views/admin/AdminSupplierIntegrationView.vue +++ b/app/resources/js/views/admin/AdminSupplierIntegrationView.vue @@ -59,7 +59,54 @@ function statusColor(status: string | null): string { return 'grey'; } -onMounted(() => void load()); +// --- Ручная очередь (ярус 3 резерва канала миграции проектов) --- + +interface ManualQueueRow { + id: number; + project_id: number; + platform: string; + operation: string; + external_id: string | null; + payload_snapshot: Record; + failure_reason: string; + created_at: string; +} + +const manualQueue = ref([]); +const manualQueueError = ref(null); +const resolvingId = ref(null); + +async function loadManualQueue(): Promise { + try { + const { data } = await axios.get('/api/admin/supplier-integration/manual-queue'); + manualQueue.value = Array.isArray(data?.queue) ? data.queue : []; + } catch { + manualQueueError.value = 'Не удалось загрузить очередь.'; + } +} + +async function resolveRow(id: number): Promise { + if (!confirm('Подтверждаете, что внесли изменения в crm.bp-gr.ru?')) return; + resolvingId.value = id; + try { + await axios.post(`/api/admin/supplier-integration/manual-queue/${id}/resolve`); + await loadManualQueue(); + } catch (e: unknown) { + const err = e as { response?: { data?: { message?: string } } }; + alert(err?.response?.data?.message ?? 'Не удалось закрыть запись.'); + } finally { + resolvingId.value = null; + } +} + +function formatDate(s: string): string { + return new Date(s).toLocaleString('ru-RU'); +} + +onMounted(() => { + void load(); + void loadManualQueue(); +}); diff --git a/app/tests/Frontend/AdminSupplierIntegrationView.manual-queue.spec.ts b/app/tests/Frontend/AdminSupplierIntegrationView.manual-queue.spec.ts new file mode 100644 index 00000000..ba42f226 --- /dev/null +++ b/app/tests/Frontend/AdminSupplierIntegrationView.manual-queue.spec.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import { createVuetify } from 'vuetify'; +import * as components from 'vuetify/components'; +import * as directives from 'vuetify/directives'; +import axios from 'axios'; +import AdminSupplierIntegrationView from '../../resources/js/views/admin/AdminSupplierIntegrationView.vue'; + +vi.mock('axios'); + +const vuetify = createVuetify({ components, directives }); + +describe('AdminSupplierIntegrationView — manual queue section', () => { + beforeEach(() => { + vi.clearAllMocks(); + (axios.get as ReturnType).mockImplementation((url: string) => { + if (url.endsWith('/manual-queue')) { + return Promise.resolve({ + data: { + queue: [ + { + id: 1, + project_id: 42, + platform: 'B1', + operation: 'create', + external_id: null, + payload_snapshot: { limit: 10, signal_type: 'site', unique_key: 'foo.com' }, + failure_reason: 'contract_break', + created_at: '2026-05-19T10:00:00Z', + }, + ], + }, + }); + } + return Promise.resolve({ data: { health: null, history: [] } }); + }); + }); + + it('renders pending queue rows with payload + reason', async () => { + const wrapper = mount(AdminSupplierIntegrationView, { global: { plugins: [vuetify] } }); + await new Promise((r) => setTimeout(r, 50)); + + const text = wrapper.text(); + expect(text).toContain('foo.com'); + expect(text).toContain('contract_break'); + expect(text).toContain('B1'); + }); + + it('clicking «Отметить выполнено» calls resolve endpoint', async () => { + (axios.post as ReturnType).mockResolvedValue({ + data: { resolved: true, external_id: 700123 }, + }); + vi.spyOn(window, 'confirm').mockReturnValue(true); + + const wrapper = mount(AdminSupplierIntegrationView, { global: { plugins: [vuetify] } }); + await new Promise((r) => setTimeout(r, 50)); + + const btn = wrapper.find('[data-testid="resolve-1"]'); + expect(btn.exists()).toBe(true); + await btn.trigger('click'); + + expect(axios.post).toHaveBeenCalledWith( + expect.stringContaining('/manual-queue/1/resolve'), + ); + }); +});