04d2346754
Task 5.1b — SalesInvoicesView (начальник, #page-invoices) поверх готовых эндпоинтов /api/sales/invoices. - Таблица счетов (дата/номер/клиент/плательщик/сумма/статус/оплатить до), поиск с debounce + фильтр статуса, чипы статусов. - Кнопка «Отметить оплаченным» (только issued/overdue) → диалог подтверждения → POST mark-paid → snackbar + перезагрузка. Зачисление баланса и Акт формирует бэк (InvoicePaymentService). - api/sales.ts: listSalesInvoices/markSalesInvoicePaid. Роут /sales/invoices со заглушки на экран. Vitest 5/5, фронт-набор 1066 без регрессий, ESLint чист. Фаза 5 завершена: начальник отмечает оплату счёта прямо в портале продаж. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
139 lines
5.6 KiB
TypeScript
139 lines
5.6 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
import { mount, flushPromises } from '@vue/test-utils';
|
||
import { createVuetify } from 'vuetify';
|
||
import { createPinia } from 'pinia';
|
||
import { createRouter, createMemoryHistory } from 'vue-router';
|
||
import SalesInvoicesView from '../../resources/js/views/sales/SalesInvoicesView.vue';
|
||
|
||
// ─── mock sales api ───────────────────────────────────────────────────────────
|
||
|
||
const listSalesInvoices = vi.fn();
|
||
const markSalesInvoicePaid = vi.fn();
|
||
|
||
vi.mock('../../resources/js/api/sales', () => ({
|
||
listSalesInvoices: (...args: unknown[]) => listSalesInvoices(...args),
|
||
markSalesInvoicePaid: (...args: unknown[]) => markSalesInvoicePaid(...args),
|
||
extractSalesErrorMessage: (_e: unknown, fallback = 'Ошибка') => fallback,
|
||
}));
|
||
|
||
function mockResponse() {
|
||
return {
|
||
data: [
|
||
{
|
||
id: 7,
|
||
invoice_number: 'СЧ-2026-0007',
|
||
amount_total: '30000',
|
||
status: 'issued',
|
||
issued_at: '2026-06-28T09:00:00+03:00',
|
||
expires_at: '2026-07-05T00:00:00+03:00',
|
||
tenant_id: 101,
|
||
tenant_name: 'Окна Москва ООО',
|
||
payer_name: 'ООО «Окна Москва»',
|
||
},
|
||
{
|
||
id: 4,
|
||
invoice_number: 'СЧ-2026-0004',
|
||
amount_total: '15000',
|
||
status: 'overdue',
|
||
issued_at: '2026-06-18T09:00:00+03:00',
|
||
expires_at: '2026-06-25T00:00:00+03:00',
|
||
tenant_id: 102,
|
||
tenant_name: 'Двери Премиум',
|
||
payer_name: 'ООО «Двери Премиум»',
|
||
},
|
||
{
|
||
id: 5,
|
||
invoice_number: 'СЧ-2026-0005',
|
||
amount_total: '25000',
|
||
status: 'paid',
|
||
issued_at: '2026-06-20T09:00:00+03:00',
|
||
expires_at: null,
|
||
tenant_id: 103,
|
||
tenant_name: 'Натяжные потолки СПб',
|
||
payer_name: 'ИП Иванов И.И.',
|
||
},
|
||
],
|
||
meta: { current_page: 1, last_page: 1, total: 3, per_page: 25 },
|
||
};
|
||
}
|
||
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
listSalesInvoices.mockResolvedValue(mockResponse());
|
||
markSalesInvoicePaid.mockResolvedValue({ status: 'ok' });
|
||
});
|
||
|
||
// ─── helpers ──────────────────────────────────────────────────────────────────
|
||
|
||
function makeRouter() {
|
||
const router = createRouter({
|
||
history: createMemoryHistory(),
|
||
routes: [
|
||
{ path: '/sales/invoices', name: 'sales-invoices', component: SalesInvoicesView },
|
||
{ path: '/sales', name: 'sales-overview', component: { template: '<div/>' } },
|
||
],
|
||
});
|
||
return router.push('/sales/invoices').then(() => router);
|
||
}
|
||
|
||
async function mountView() {
|
||
const router = await makeRouter();
|
||
const wrapper = mount(SalesInvoicesView, {
|
||
global: { plugins: [createVuetify(), createPinia(), router] },
|
||
});
|
||
await flushPromises();
|
||
return wrapper;
|
||
}
|
||
|
||
describe('SalesInvoicesView', () => {
|
||
it('вызывает listSalesInvoices при монтировании', async () => {
|
||
await mountView();
|
||
expect(listSalesInvoices).toHaveBeenCalled();
|
||
});
|
||
|
||
it('отрисовывает строки счетов из мока', async () => {
|
||
const wrapper = await mountView();
|
||
const text = wrapper.text();
|
||
expect(text).toContain('СЧ-2026-0007');
|
||
expect(text).toContain('Окна Москва ООО');
|
||
expect(text).toContain('ООО «Окна Москва»');
|
||
expect(text).toContain('СЧ-2026-0004');
|
||
expect(text).toContain('Двери Премиум');
|
||
expect(text).toContain('СЧ-2026-0005');
|
||
});
|
||
|
||
it('показывает статус-чипы (Выставлен / Просрочен / Оплачен)', async () => {
|
||
const wrapper = await mountView();
|
||
const text = wrapper.text();
|
||
expect(text).toContain('Выставлен');
|
||
expect(text).toContain('Просрочен');
|
||
expect(text).toContain('Оплачен');
|
||
});
|
||
|
||
it('для issued/overdue есть кнопка «Отметить оплаченным», для paid — нет', async () => {
|
||
const wrapper = await mountView();
|
||
expect(wrapper.find('[data-testid="mark-paid-7"]').exists()).toBe(true);
|
||
expect(wrapper.find('[data-testid="mark-paid-4"]').exists()).toBe(true);
|
||
expect(wrapper.find('[data-testid="mark-paid-5"]').exists()).toBe(false);
|
||
});
|
||
|
||
it('клик «Отметить оплаченным» открывает диалог, подтверждение вызывает markSalesInvoicePaid с id + перезагрузку', async () => {
|
||
const wrapper = await mountView();
|
||
expect(listSalesInvoices).toHaveBeenCalledTimes(1);
|
||
|
||
await wrapper.find('[data-testid="mark-paid-7"]').trigger('click');
|
||
await flushPromises();
|
||
|
||
// диалог открыт
|
||
expect(wrapper.vm.confirmOpen).toBe(true);
|
||
|
||
// подтверждаем
|
||
await wrapper.vm.doMarkPaid();
|
||
await flushPromises();
|
||
|
||
expect(markSalesInvoicePaid).toHaveBeenCalledWith(7);
|
||
// перезагрузка списка после отметки оплаты
|
||
expect(listSalesInvoices).toHaveBeenCalledTimes(2);
|
||
});
|
||
});
|