Files
portal/app/tests/Frontend/SalesIncome.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

140 lines
5.7 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 SalesIncomeView from '../../resources/js/views/sales/SalesIncomeView.vue';
// ─── mock api/sales ───────────────────────────────────────────────────────────
vi.mock('../../resources/js/api/sales', () => {
const income = {
totals: {
oborot_rub: 235050,
accrued_rub: 13350,
paid_all_time_rub: 85000,
to_pay_period_rub: 13350,
},
per_client: [
{
tenant_id: 101,
organization_name: 'Кухни на заказ Екб',
tariff_kind: 'topup_step',
tariff_label: 'Пополнения·Станд.',
topups_rub: 40000,
earned_rub: 4000,
},
{
tenant_id: 202,
organization_name: 'Натяжные потолки СПб',
tariff_kind: 'topup_step',
tariff_label: 'Пополнения·Агро',
topups_rub: 25000,
earned_rub: 1500,
},
],
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: 11,
manager_name: 'Алексей Смирнов',
amount_rub: 25000,
paid_on: '2026-06-10',
comment: 'Аванс',
created_at: '2026-06-10T09:00:00Z',
creator_name: 'Виктор Орлов',
},
],
};
return {
getSalesIncome: vi.fn().mockResolvedValue(income),
extractSalesErrorMessage: vi.fn().mockReturnValue('Произошла ошибка. Попробуйте позже.'),
};
});
beforeEach(() => vi.clearAllMocks());
// ─── helpers ──────────────────────────────────────────────────────────────────
function makeRouter() {
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/sales/income', name: 'sales-income', component: SalesIncomeView },
{ path: '/sales', name: 'sales-overview', component: { template: '<div/>' } },
],
});
return router.push('/sales/income').then(() => router);
}
async function mountView() {
const router = await makeRouter();
const wrapper = mount(SalesIncomeView, {
global: { plugins: [createVuetify(), createPinia(), router] },
});
await flushPromises();
return wrapper;
}
describe('SalesIncomeView', () => {
it('вызывает getSalesIncome при монтировании', async () => {
await mountView();
const api = await import('../../resources/js/api/sales');
expect(api.getSalesIncome).toHaveBeenCalled();
});
it('рендерит заголовок «Мой доход» и 4 KPI', async () => {
const wrapper = await mountView();
const text = wrapper.text();
expect(text).toContain('Мой доход');
expect(text).toContain('Оборот моих клиентов');
expect(text).toContain('Начислено мне');
expect(text).toContain('Выплачено');
expect(text).toContain('К выплате');
// Значения (ru-RU форматирование)
expect(text).toContain('235');
expect(text).toContain('13');
expect(text).toContain('85');
const tiles = wrapper.findAll('[data-testid="income-kpi"]');
expect(tiles.length).toBe(4);
});
it('рендерит таблицу «Начислено по клиентам» с итогом', async () => {
const wrapper = await mountView();
const rows = wrapper.findAll('[data-testid="per-client-row"]');
expect(rows.length).toBe(2);
const text = wrapper.text();
expect(text).toContain('Кухни на заказ Екб');
expect(text).toContain('Натяжные потолки СПб');
expect(text).toContain('Пополнения·Станд.');
// ИТОГО earned = 4000 + 1500 = 5500
const total = wrapper.find('[data-testid="per-client-total"]');
expect(total.exists()).toBe(true);
expect(total.text()).toContain('5');
});
it('рендерит журнал выплат мне с итогом', async () => {
const wrapper = await mountView();
const rows = wrapper.findAll('[data-testid="income-payout-row"]');
expect(rows.length).toBe(2);
const text = wrapper.text();
expect(text).toContain('Зарплата за июнь, часть 1');
expect(text).toContain('Аванс');
expect(text).toContain('Виктор Орлов');
// ИТОГО выплачено = 35000 + 25000 = 60000
const total = wrapper.find('[data-testid="income-payout-total"]');
expect(total.exists()).toBe(true);
expect(total.text()).toContain('60');
});
});