339e8ea53b
Закрыт TODO (c) из v1.50: backend-эндпоинт для списка сделок и его
интеграция в DealsView/KanbanView вместо статичного MOCK_DEALS.
Backend (DealController::index):
- Query-params: tenant_id (required, 422/404), status_in[] (whereIn),
project_id, manager_id, search (ILIKE по phone+contact_name),
limit clamp [1..500] default 100, offset default 0.
- ORDER BY received_at DESC, id DESC. Eager-load project + manager.
- RLS-обёртка SET LOCAL app.current_tenant_id + defense-in-depth
where(tenant_id) — на тестах через postgres superuser RLS обходится
BYPASSRLS, app-фильтр гарантирует изоляцию.
- Ответ: {deals: [...], total, limit, offset}; manager_name/initials
форматируются через ManagerController::formatName/formatInitials.
Pest +12 (DealIndexTest):
- 422/404, пустой список, relations (project_name+manager_name+initials),
RLS-изоляция, ORDER BY, status_in[], project_id, manager_id, search
ILIKE, limit+offset, manager=null edge case.
Frontend:
- api/deals.ts::listDeals — типизированный helper c ApiDeal/ListDeals*.
- composables/dealsApiMapper.ts::mapApiDeal — converter ApiDeal→MockDeal:
contact_name fallback на phone, manager.name='Не назначен' /
initials='—' при null, project='—' при null, cost=0,
receivedMinutesAgo=max(0, …) от clock-skew.
- DealsView/KanbanView: onMounted(loadDeals) async-вызывает listDeals
если auth.user.tenant_id, на success replace через splice, на fail
fetchError=true + v-alert warning, MOCK_DEALS как fallback.
Vitest +14:
- dealsApiMapper.spec.ts (8): 1:1, fallback'и, edge cases.
- DealsListIntegration.spec.ts (6): без tenant_id — НЕ вызывает API,
с tenant_id — replace state, reject → fetchError + alert + fallback;
для DealsView и KanbanView.
PHPStan baseline регенерирован. cspell-glossary +ILIKE +DTO.
Регресс:
- Lint+type-check+format passed.
- Vitest 261/261 за 19.62 сек (+14 от 247).
- Vite build 989 ms.
- Pint + PHPStan passed.
- Pest 186/186 за 22 сек (+12 от 174, 742 assertions).
Реестр v1.59→v1.60 / CLAUDE.md v1.50→v1.51.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { mapApiDeal } from '../../resources/js/composables/dealsApiMapper';
|
||
import type { ApiDeal } from '../../resources/js/api/deals';
|
||
|
||
describe('mapApiDeal', () => {
|
||
const baseApi: ApiDeal = {
|
||
id: 42,
|
||
tenant_id: 1,
|
||
project_id: 7,
|
||
project_name: 'Окна Москва',
|
||
phone: '+7 (999) 111-11-11',
|
||
contact_name: 'Анна С.',
|
||
status: 'new',
|
||
manager_id: 3,
|
||
manager_name: 'Иван П.',
|
||
manager_initials: 'ИП',
|
||
received_at: '2026-05-09T10:00:00Z',
|
||
};
|
||
|
||
it('маппит обязательные поля 1:1', () => {
|
||
const m = mapApiDeal(baseApi, new Date('2026-05-09T10:30:00Z'));
|
||
expect(m.id).toBe(42);
|
||
expect(m.phone).toBe('+7 (999) 111-11-11');
|
||
expect(m.statusSlug).toBe('new');
|
||
expect(m.project).toBe('Окна Москва');
|
||
expect(m.manager.name).toBe('Иван П.');
|
||
expect(m.manager.initials).toBe('ИП');
|
||
});
|
||
|
||
it('name берёт contact_name; fallback на phone когда contact_name = null', () => {
|
||
const m1 = mapApiDeal(baseApi);
|
||
expect(m1.name).toBe('Анна С.');
|
||
|
||
const m2 = mapApiDeal({ ...baseApi, contact_name: null });
|
||
expect(m2.name).toBe('+7 (999) 111-11-11');
|
||
});
|
||
|
||
it('manager.name = «Не назначен» / initials = «—» если manager null', () => {
|
||
const m = mapApiDeal({ ...baseApi, manager_id: null, manager_name: null, manager_initials: null });
|
||
expect(m.manager.name).toBe('Не назначен');
|
||
expect(m.manager.initials).toBe('—');
|
||
});
|
||
|
||
it('project = «—» если project_name null', () => {
|
||
const m = mapApiDeal({ ...baseApi, project_name: null });
|
||
expect(m.project).toBe('—');
|
||
});
|
||
|
||
it("cost всегда 0 (отдельного endpoint'а пока нет)", () => {
|
||
const m = mapApiDeal(baseApi);
|
||
expect(m.cost).toBe(0);
|
||
});
|
||
|
||
it('receivedMinutesAgo вычисляется из received_at', () => {
|
||
const m = mapApiDeal(baseApi, new Date('2026-05-09T10:30:00Z'));
|
||
expect(m.receivedMinutesAgo).toBe(30);
|
||
});
|
||
|
||
it('receivedMinutesAgo не уходит в отрицательные числа при future timestamp', () => {
|
||
const m = mapApiDeal(baseApi, new Date('2026-05-09T09:00:00Z'));
|
||
expect(m.receivedMinutesAgo).toBe(0);
|
||
});
|
||
|
||
it('received_at = null → 0 минут (используем "сейчас")', () => {
|
||
const m = mapApiDeal({ ...baseApi, received_at: null }, new Date('2026-05-09T10:30:00Z'));
|
||
expect(m.receivedMinutesAgo).toBe(0);
|
||
});
|
||
});
|