feat(supplier): manual queue section in AdminSupplierIntegrationView
Таблица pending-записей яруса 3 + кнопка «Отметить выполнено» с confirm-
диалогом, дёргает POST .../manual-queue/{id}/resolve. Реюз существующего
админ-экрана интеграции с поставщиком (после «Истории сверок»).
NB: spec в tests/Frontend/ (vitest include — tests/Frontend/**, не
resources/.../__tests__/ как указал план Step 11.1). loadManualQueue
defensive Array.isArray-guard — иначе onMounted в чужих spec'ах
(mockResolvedValue без queue-ключа) ловил undefined.length.
Spec §4.6. Task 11 of 12. Vitest 5/5 (2 новых + 3 существующих).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, unknown>;
|
||||
failure_reason: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const manualQueue = ref<ManualQueueRow[]>([]);
|
||||
const manualQueueError = ref<string | null>(null);
|
||||
const resolvingId = ref<number | null>(null);
|
||||
|
||||
async function loadManualQueue(): Promise<void> {
|
||||
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<void> {
|
||||
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();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -130,5 +177,59 @@ onMounted(() => void load());
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-card>
|
||||
|
||||
<v-card class="mt-4">
|
||||
<v-card-title>
|
||||
Ручная очередь
|
||||
<v-chip v-if="manualQueue.length" color="warning" class="ml-2" size="small">
|
||||
{{ manualQueue.length }}
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-alert v-if="manualQueueError" type="error" density="compact">
|
||||
{{ manualQueueError }}
|
||||
</v-alert>
|
||||
<p v-else-if="!manualQueue.length" class="text-medium-emphasis">
|
||||
Очередь пуста — авто-фейловер не понадобился.
|
||||
</p>
|
||||
<v-table v-else density="compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project</th>
|
||||
<th>Платформа</th>
|
||||
<th>Операция</th>
|
||||
<th>Параметры</th>
|
||||
<th>Причина</th>
|
||||
<th>Создано</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in manualQueue" :key="row.id">
|
||||
<td>#{{ row.project_id }}</td>
|
||||
<td>{{ row.platform }}</td>
|
||||
<td>{{ row.operation }}</td>
|
||||
<td>
|
||||
<code>{{ row.payload_snapshot.unique_key }}</code>
|
||||
· limit {{ row.payload_snapshot.limit ?? '—' }}
|
||||
</td>
|
||||
<td>{{ row.failure_reason }}</td>
|
||||
<td>{{ formatDate(row.created_at) }}</td>
|
||||
<td>
|
||||
<v-btn
|
||||
size="small"
|
||||
color="primary"
|
||||
:data-testid="`resolve-${row.id}`"
|
||||
:loading="resolvingId === row.id"
|
||||
@click="resolveRow(row.id)"
|
||||
>
|
||||
Отметить выполнено
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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<typeof vi.fn>).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<typeof vi.fn>).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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user