d5112bd375
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>
140 lines
5.7 KiB
TypeScript
140 lines
5.7 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 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');
|
||
});
|
||
});
|