Files
portal/app/tests/Frontend/SalesPayouts.spec.ts
T
Дмитрий d5112bd375 feat(sales): экраны выплат и «Мой доход» (фронт)
Task 4.2b — два экрана поверх готовых эндпоинтов.

- SalesPayoutsView (начальник, #page-payouts): форма «Провести выплату»
  (менеджер из remaining, сумма, дата, комментарий) → POST /payouts →
  snackbar + перезагрузка; таблица «Остаток к выплате по менеджерам»
  (начислено/выплачено/остаток за период); «Журнал всех выплат».
- SalesIncomeView (менеджер, #page-income): 4 KPI (оборот/начислено/
  выплачено всего/к выплате), таблица «Начислено по клиентам» с ИТОГО,
  «Журнал выплат мне» с ИТОГО.
- api/sales.ts: listSalesPayouts/getSalesPayoutsRemaining/createSalesPayout/
  getSalesIncome. Роуты /sales/payouts и /sales/income со заглушек на экраны.

Vitest 9/9, фронт-набор 1061 без регрессий, ESLint чист.

Фаза 4 завершена: начальник проводит выплаты, менеджер видит начисления
и выплаты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:29:32 +03:00

156 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 SalesPayoutsView from '../../resources/js/views/sales/SalesPayoutsView.vue';
// ─── mock api/sales ───────────────────────────────────────────────────────────
vi.mock('../../resources/js/api/sales', () => {
const remaining = [
{
manager_id: 11,
name: 'Алексей Смирнов',
clients_count: 7,
oborot_rub: 235050,
accrued_rub: 13350,
paid_period_rub: 60000,
paid_all_time_rub: 85000,
remaining_rub: -71650,
},
{
manager_id: 22,
name: 'Мария Котова',
clients_count: 5,
oborot_rub: 165300,
accrued_rub: 54795,
paid_period_rub: 30000,
paid_all_time_rub: 60000,
remaining_rub: -5205,
},
];
const payouts = [
{
id: 1,
sales_user_id: 11,
manager_name: 'Алексей Смирнов',
amount_rub: 35000,
paid_on: '2026-06-25',
comment: 'Зарплата за июнь, часть 1',
created_at: '2026-06-25T09:00:00Z',
creator_name: 'Виктор Орлов',
},
{
id: 2,
sales_user_id: 22,
manager_name: 'Мария Котова',
amount_rub: 30000,
paid_on: '2026-06-25',
comment: 'Зарплата за июнь',
created_at: '2026-06-25T09:05:00Z',
creator_name: 'Виктор Орлов',
},
];
return {
getSalesPayoutsRemaining: vi.fn().mockResolvedValue(remaining),
listSalesPayouts: vi.fn().mockResolvedValue(payouts),
createSalesPayout: vi.fn().mockResolvedValue({
id: 99,
sales_user_id: 11,
manager_name: 'Алексей Смирнов',
amount_rub: 12345,
paid_on: '2026-07-02',
comment: 'Тест',
created_at: '2026-07-02T10:00:00Z',
creator_name: 'Виктор Орлов',
}),
extractSalesErrorMessage: vi.fn().mockReturnValue('Произошла ошибка. Попробуйте позже.'),
};
});
beforeEach(() => vi.clearAllMocks());
// ─── helpers ──────────────────────────────────────────────────────────────────
function makeRouter() {
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/sales/payouts', name: 'sales-payouts', component: SalesPayoutsView },
{ path: '/sales', name: 'sales-overview', component: { template: '<div/>' } },
],
});
return router.push('/sales/payouts').then(() => router);
}
async function mountView() {
const router = await makeRouter();
const wrapper = mount(SalesPayoutsView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
return wrapper;
}
describe('SalesPayoutsView', () => {
it('вызывает getSalesPayoutsRemaining и listSalesPayouts при монтировании', async () => {
await mountView();
const api = await import('../../resources/js/api/sales');
expect(api.getSalesPayoutsRemaining).toHaveBeenCalled();
expect(api.listSalesPayouts).toHaveBeenCalled();
});
it('рендерит таблицу остатка к выплате по менеджерам', async () => {
const wrapper = await mountView();
const text = wrapper.text();
expect(text).toContain('Остаток к выплате');
expect(text).toContain('Алексей Смирнов');
expect(text).toContain('Мария Котова');
// clients_count и суммы
expect(text).toContain('235');
expect(text).toContain('54');
const rows = wrapper.findAll('[data-testid="remaining-row"]');
expect(rows.length).toBe(2);
});
it('рендерит журнал всех выплат', async () => {
const wrapper = await mountView();
const rows = wrapper.findAll('[data-testid="payout-row"]');
expect(rows.length).toBe(2);
const text = wrapper.text();
expect(text).toContain('Зарплата за июнь, часть 1');
expect(text).toContain('Зарплата за июнь');
});
it('кнопка «Провести» вызывает createSalesPayout с верным payload и перезагружает', async () => {
const wrapper = await mountView();
const api = await import('../../resources/js/api/sales');
// Выбираем менеджера, сумму, дату, комментарий через модель компонента.
wrapper.vm.form.manager_id = 11;
wrapper.vm.form.amount_rub = 12345;
wrapper.vm.form.paid_on = '2026-07-02';
wrapper.vm.form.comment = 'Тест';
await flushPromises();
await wrapper.find('[data-testid="payout-submit"]').trigger('click');
await flushPromises();
expect(api.createSalesPayout).toHaveBeenCalledWith({
manager_id: 11,
amount_rub: 12345,
paid_on: '2026-07-02',
comment: 'Тест',
});
// после успешного создания — перезагрузка remaining (2 раза: mount + reload)
expect((api.getSalesPayoutsRemaining as ReturnType<typeof vi.fn>).mock.calls.length).toBeGreaterThanOrEqual(2);
expect((api.listSalesPayouts as ReturnType<typeof vi.fn>).mock.calls.length).toBeGreaterThanOrEqual(2);
});
it('заголовок «Выплаты менеджерам» отрисован', async () => {
const wrapper = await mountView();
expect(wrapper.text()).toContain('Выплаты менеджерам');
});
});