feat(deals): убрать префикс B1_/B2_/B3_ из отображения «Источник»
Поставщик crm.bp префиксует имена проектов признаком канала-провайдера (B1_/B2_/B3_ — три базы лидов). В UI Лидерры префикс — шум: пользователю интересен сам проект, не канал. Трансформация display-only — данные в БД не трогаем, фильтрация идёт по project_id (не name). Утилита: app/resources/js/composables/projectName.ts → stripChannelPrefix. Регэксп ^B[123]_ case-insensitive; null/undefined/'' → ''. Применено в 4 точках: - DealsTable «Источник» (item.project) - DealsFilters «Проект» dropdown (через computed-маппинг в DealsView) - KanbanCard карточка - DealDetailBody параметры панели Тесты: 8 unit-тестов на утилиту (B1/B2/B3 case-insensitive, не трогать B0/B4/Bx, не трогать префикс в середине строки, null/undefined/''), 38/38 на затронутых компонентах, 868/3sk/0 full Vitest, build 2.62s. Smoke /deals: 20 строк, ни одна не начинается с B1_/B2_/B3_ (был «B1_73912557675 [35]», стал «73912557675 [35]»; «B3_krk-finance.ru/...» → «krk-finance.ru/...»). Скриншот deals-no-bprefix-2026-05-18.png. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||
import type { MockDeal } from '../../composables/mockDeals';
|
||||
import { type DealEvent } from '../../composables/mockDealEvents';
|
||||
import { mapApiDealEvent } from '../../composables/dealsApiMapper';
|
||||
import { stripChannelPrefix } from '../../composables/projectName';
|
||||
import * as dealsApi from '../../api/deals';
|
||||
import * as remindersApi from '../../api/reminders';
|
||||
import type { ApiReminder } from '../../api/reminders';
|
||||
@@ -162,7 +163,7 @@ defineExpose({
|
||||
<dl class="params">
|
||||
<div class="param">
|
||||
<dt class="text-caption text-medium-emphasis">Проект</dt>
|
||||
<dd class="text-body-2">{{ deal.project }}</dd>
|
||||
<dd class="text-body-2">{{ stripChannelPrefix(deal.project) }}</dd>
|
||||
</div>
|
||||
<div class="param">
|
||||
<dt class="text-caption text-medium-emphasis">Стоимость лида</dt>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
import type { MockDeal } from '../../composables/mockDeals';
|
||||
import type { LeadStatus } from '../../composables/leadStatuses';
|
||||
import { stripChannelPrefix } from '../../composables/projectName';
|
||||
import StatusPill from '../ui/StatusPill.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -71,7 +72,7 @@ function rowProps(deal: MockDeal): Record<string, unknown> {
|
||||
|
||||
<template #[`item.project`]="{ item }: { item: MockDeal }">
|
||||
<div class="cell-source">
|
||||
<span class="source-project">{{ item.project }}</span>
|
||||
<span class="source-project">{{ stripChannelPrefix(item.project) }}</span>
|
||||
<span v-if="signalLabel(item.signalType)" class="source-signal">{{
|
||||
signalLabel(item.signalType)
|
||||
}}</span>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* Click → emit('open', deal.id) — TODO: правая панель DealDetailDrawer.
|
||||
*/
|
||||
import type { MockDeal } from '../../composables/mockDeals';
|
||||
import { stripChannelPrefix } from '../../composables/projectName';
|
||||
|
||||
defineProps<{ deal: MockDeal }>();
|
||||
const emit = defineEmits<{ open: [id: number] }>();
|
||||
@@ -27,7 +28,7 @@ function formatCost(cost: number): string {
|
||||
<div class="card-name">{{ deal.name }}</div>
|
||||
<div class="card-phone text-caption text-medium-emphasis">{{ deal.phone }}</div>
|
||||
<div class="card-meta mt-2">
|
||||
<span class="card-project text-caption">{{ deal.project }}</span>
|
||||
<span class="card-project text-caption">{{ stripChannelPrefix(deal.project) }}</span>
|
||||
<span class="card-cost num">{{ formatCost(deal.cost) }}</span>
|
||||
</div>
|
||||
<div class="card-foot mt-1">
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Утилиты отображения имён проектов crm.bp.
|
||||
*
|
||||
* Поставщик crm.bp префиксует имена проектов признаком канала-провайдера
|
||||
* (B1_/B2_/B3_ — три разных базы лидов). В UI Лидерры префикс — шум:
|
||||
* пользователю интересен сам проект, а не канал.
|
||||
*
|
||||
* Трансформация — **display-only**: данные в БД (`supplier_projects.name`)
|
||||
* не трогаем, фильтрация/поиск/маппинг идёт по сырому имени и `id`.
|
||||
*/
|
||||
|
||||
const CHANNEL_PREFIX_RE = /^B[123]_/i;
|
||||
|
||||
/**
|
||||
* Убирает префикс B1_/B2_/B3_ из начала имени проекта (case-insensitive).
|
||||
* Префикс внутри строки и другие буквы (B0/B4/Bx) не трогает.
|
||||
* null/undefined/'' -> ''.
|
||||
*/
|
||||
export function stripChannelPrefix(name: string | null | undefined): string {
|
||||
if (!name) return '';
|
||||
return name.replace(CHANNEL_PREFIX_RE, '');
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router';
|
||||
import type { MockDeal } from '../composables/mockDeals';
|
||||
import { mapApiDeal } from '../composables/dealsApiMapper';
|
||||
import { stripChannelPrefix } from '../composables/projectName';
|
||||
import { usePolling } from '../composables/usePolling';
|
||||
import DealsFilters from '../components/deals/DealsFilters.vue';
|
||||
import DealsBulkBar from '../components/deals/DealsBulkBar.vue';
|
||||
@@ -46,6 +47,11 @@ const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const fetchError = ref(false);
|
||||
const availableProjects = ref<dealsApi.ApiProject[]>([]);
|
||||
// Список для фильтра «Проект» — без префикса B1_/B2_/B3_ (display-only;
|
||||
// id сохраняем, фильтрация идёт по id, не по name).
|
||||
const availableProjectsForFilter = computed(() =>
|
||||
availableProjects.value.map((p) => ({ ...p, name: stripChannelPrefix(p.name) })),
|
||||
);
|
||||
|
||||
const leadStatuses = computed(() => leadStatusesStore.statuses);
|
||||
const statusBySlug = computed(() => leadStatusesStore.bySlug);
|
||||
@@ -297,7 +303,7 @@ defineExpose({
|
||||
v-model:filter-project="filterProject"
|
||||
v-model:filter-city="filterCity"
|
||||
:lead-statuses="leadStatuses"
|
||||
:available-projects="availableProjects"
|
||||
:available-projects="availableProjectsForFilter"
|
||||
:available-cities="availableCities"
|
||||
class="mt-4"
|
||||
@clear-filters="clearFilters"
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { stripChannelPrefix } from '../../resources/js/composables/projectName';
|
||||
|
||||
/**
|
||||
* Имена проектов crm.bp префиксуются B1_/B2_/B3_ (источник-провайдер).
|
||||
* В UI Лидерры префикс убираем — он шум для пользователя; данные в БД не трогаем.
|
||||
*/
|
||||
describe('stripChannelPrefix', () => {
|
||||
it('убирает B1_ префикс', () => {
|
||||
expect(stripChannelPrefix('B1_73912557675 [35]')).toBe('73912557675 [35]');
|
||||
});
|
||||
|
||||
it('убирает B2_ префикс', () => {
|
||||
expect(stripChannelPrefix('B2_krk-finance.ru/cabinet/auth [24]')).toBe('krk-finance.ru/cabinet/auth [24]');
|
||||
});
|
||||
|
||||
it('убирает B3_ префикс', () => {
|
||||
expect(stripChannelPrefix('B3_kras.vashinvestor.ru [23]')).toBe('kras.vashinvestor.ru [23]');
|
||||
});
|
||||
|
||||
it('case-insensitive: b1_/b2_/b3_ тоже убирает', () => {
|
||||
expect(stripChannelPrefix('b1_test')).toBe('test');
|
||||
expect(stripChannelPrefix('b3_demo')).toBe('demo');
|
||||
});
|
||||
|
||||
it('не трогает имя без префикса', () => {
|
||||
expect(stripChannelPrefix('quidem fugiat unde')).toBe('quidem fugiat unde');
|
||||
expect(stripChannelPrefix('Натяжные потолки')).toBe('Натяжные потолки');
|
||||
});
|
||||
|
||||
it('не трогает B4_/B0_/Bx_ — только B1/B2/B3', () => {
|
||||
expect(stripChannelPrefix('B4_other')).toBe('B4_other');
|
||||
expect(stripChannelPrefix('B0_zero')).toBe('B0_zero');
|
||||
expect(stripChannelPrefix('BX_unknown')).toBe('BX_unknown');
|
||||
});
|
||||
|
||||
it('не трогает префикс внутри строки — только в начале', () => {
|
||||
expect(stripChannelPrefix('foo B1_bar')).toBe('foo B1_bar');
|
||||
});
|
||||
|
||||
it('терпит null/undefined/пустую строку', () => {
|
||||
expect(stripChannelPrefix(null)).toBe('');
|
||||
expect(stripChannelPrefix(undefined)).toBe('');
|
||||
expect(stripChannelPrefix('')).toBe('');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user