From ca9615a1fd17bef81ba4ed9828c3df5a78f906e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Wed, 24 Jun 2026 13:03:55 -0300 Subject: [PATCH] Clean seller metadata display names --- backend/services/analyticsService.js | 24 ++++++++++++++++++++---- backend/test/analyticsService.test.js | 6 +++++- src/displayFormatters.ts | 26 ++++++++++++++++++++++++++ src/pages/ClientDetails.tsx | 10 ++++------ src/pages/Clients.tsx | 4 ++-- 5 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 src/displayFormatters.ts diff --git a/backend/services/analyticsService.js b/backend/services/analyticsService.js index 664a42b..a3c0d96 100644 --- a/backend/services/analyticsService.js +++ b/backend/services/analyticsService.js @@ -42,6 +42,7 @@ const CUSTOMER_IDENTITY_CTE = ` ) `; const CUSTOMER_KEY_SQL = 'customer_key'; +const TRAILING_SELLER_ID_SQL_PATTERN = '[[:space:]]*#([0-9]+)[[:space:]]*$'; const getClientTokenSecret = () => ( process.env.CLIENT_TOKEN_SECRET || @@ -196,6 +197,19 @@ const normalizeSellerFilter = (value) => { return { type: 'any', value: normalizedValue }; }; +const normalizeSellerOption = (row) => { + const rawId = String(row.id || '').trim(); + const rawName = String(row.name || '').trim(); + const idFromName = rawName.match(/#(\d+)\s*$/)?.[1] || ''; + const id = rawId || idFromName; + const name = rawName.replace(/#\d+\s*$/, '').trim(); + + return { + id, + name: name || id + }; +}; + const appendOrderMetadataFilters = (params, filters, range = {}) => { const marketplace = normalizeTextFilter(range.marketplace); const salesChannel = normalizeTextFilter(range.canal_venda || range.canalVenda); @@ -703,8 +717,11 @@ const getClientFilterOptions = async () => { pool.query(` WITH seller_options AS ( SELECT - NULLIF(TRIM(id_vendedor), '') as id, - NULLIF(TRIM(nome_vendedor), '') as name + COALESCE( + NULLIF(TRIM(id_vendedor), ''), + substring(NULLIF(TRIM(nome_vendedor), '') from '${TRAILING_SELLER_ID_SQL_PATTERN}') + ) as id, + NULLIF(TRIM(regexp_replace(COALESCE(nome_vendedor, ''), '${TRAILING_SELLER_ID_SQL_PATTERN}', '')), '') as name FROM orders WHERE ( NULLIF(TRIM(id_vendedor), '') IS NOT NULL @@ -720,8 +737,7 @@ const getClientFilterOptions = async () => { const sellerOptionsByValue = new Map(); sellerResult.rows.forEach(row => { - const id = row.id || ''; - const name = row.name || ''; + const { id, name } = normalizeSellerOption(row); const value = id ? `id:${id}` : `name:${name}`; if (!value || sellerOptionsByValue.has(value)) return; diff --git a/backend/test/analyticsService.test.js b/backend/test/analyticsService.test.js index 998cb18..230dae7 100644 --- a/backend/test/analyticsService.test.js +++ b/backend/test/analyticsService.test.js @@ -641,6 +641,7 @@ test('getClientFilterOptions returns all distinct order metadata options', async rows: [ { id: 'VEN-1', name: 'Maria' }, { id: '', name: 'Sem ID' }, + { id: '', name: 'KEDMA DA SILVA #977226210' }, { id: 'VEN-1', name: 'Maria' } ] }; @@ -657,12 +658,15 @@ test('getClientFilterOptions returns all distinct order metadata options', async }); assert.match(calls[0].sql, /WHERE NULLIF\(TRIM\(marketplace\), ''\) IS NOT NULL/); assert.match(calls[2].sql, /WITH seller_options AS/); + assert.match(calls[2].sql, /substring\(NULLIF\(TRIM\(nome_vendedor\), ''\) from/); + assert.match(calls[2].sql, /regexp_replace\(COALESCE\(nome_vendedor, ''\)/); assert.match(calls[2].sql, /NULLIF\(TRIM\(nome_vendedor\), ''\) IS NOT NULL/); assert.deepEqual(options.marketplaces, ['Mercado Livre', 'Shopee']); assert.deepEqual(options.salesChannels, ['Online']); assert.deepEqual(options.sellers, [ { value: 'id:VEN-1', id: 'VEN-1', name: 'Maria' }, - { value: 'name:Sem ID', id: '', name: 'Sem ID' } + { value: 'name:Sem ID', id: '', name: 'Sem ID' }, + { value: 'id:977226210', id: '977226210', name: 'KEDMA DA SILVA' } ]); } finally { pool.query = originalQuery; diff --git a/src/displayFormatters.ts b/src/displayFormatters.ts new file mode 100644 index 0000000..3521509 --- /dev/null +++ b/src/displayFormatters.ts @@ -0,0 +1,26 @@ +const SMALL_WORDS = new Set(['da', 'de', 'do', 'das', 'dos', 'e']); + +export const removeTrailingSellerId = (value: string) => { + return value.replace(/\s*#\d+\s*$/, '').trim(); +}; + +export const formatDisplayName = (value: string) => { + const normalized = String(value || '').replace(/\s+/g, ' ').trim(); + if (!normalized) return ''; + + const withoutSellerId = removeTrailingSellerId(normalized); + const withoutNumericPrefix = withoutSellerId.replace(/^\[\d+\]\s*/, '').trim(); + const isUppercaseName = /[A-ZÁÀÂÃÉÈÊÍÏÓÔÕÖÚÇÑ]/.test(withoutNumericPrefix) && + withoutNumericPrefix === withoutNumericPrefix.toUpperCase(); + + if (!isUppercaseName) return withoutNumericPrefix; + + return withoutNumericPrefix + .toLocaleLowerCase('pt-BR') + .split(' ') + .map((word, index) => { + if (index > 0 && SMALL_WORDS.has(word)) return word; + return word.charAt(0).toLocaleUpperCase('pt-BR') + word.slice(1); + }) + .join(' '); +}; diff --git a/src/pages/ClientDetails.tsx b/src/pages/ClientDetails.tsx index 9b65bb5..399d1be 100644 --- a/src/pages/ClientDetails.tsx +++ b/src/pages/ClientDetails.tsx @@ -5,6 +5,7 @@ import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContaine import DateRangePicker from '../components/DateRangePicker'; import type { ClientDetailsAnalytics, DateRange, OrderData } from '../types'; import { fetchClientDetailsAnalytics } from '../dataService'; +import { formatDisplayName, removeTrailingSellerId } from '../displayFormatters'; type CustomTooltipProps = { active?: boolean; @@ -27,14 +28,11 @@ const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => { }; const getOrderMetadata = (order: OrderData) => { - const seller = [ - order.nome_vendedor, - order.id_vendedor ? `#${order.id_vendedor}` : '' - ].filter(Boolean).join(' '); + const sellerName = formatDisplayName(removeTrailingSellerId(order.nome_vendedor || '')); return [ - order.cliente_nome_fantasia ? `Fantasia: ${order.cliente_nome_fantasia}` : '', - seller ? `Vendedor: ${seller}` : '', + order.cliente_nome_fantasia ? `Fantasia: ${formatDisplayName(order.cliente_nome_fantasia)}` : '', + sellerName ? `Vendedor: ${sellerName}` : '', order.marketplace ? `Marketplace: ${order.marketplace}` : '', order.canal_venda ? `Canal: ${order.canal_venda}` : '', order.numero_ecommerce ? `E-commerce: ${order.numero_ecommerce}` : '' diff --git a/src/pages/Clients.tsx b/src/pages/Clients.tsx index 4258e1a..2354eca 100644 --- a/src/pages/Clients.tsx +++ b/src/pages/Clients.tsx @@ -5,6 +5,7 @@ import type { ClientAnalyticsItem, ClientFilterOptions, ClientMetadataFilters, D import { fetchClientAnalytics, fetchClientFilterOptions, fetchRfmAnalytics, getCachedClientAnalytics, getCachedClientFilterOptions, getCachedRfmAnalytics } from '../dataService'; import { endOfLocalDay, formatDateParam, parseLocalDateInput, rangeForDay, rangeForLastDays, rangeForPreviousDay, startOfLocalDay } from '../dateRanges'; import type { ClientSortOption, ClientSummary } from '../analytics/clients'; +import { formatDisplayName, removeTrailingSellerId } from '../displayFormatters'; const clientTypeStyles: Record = { 'Sem análise': 'border-zinc-600/30 bg-zinc-600/15 text-zinc-300', @@ -318,8 +319,7 @@ const Clients = () => { }; const getSellerOptionLabel = (seller: ClientFilterOptions['sellers'][number]) => { - if (seller.id && seller.name && seller.id !== seller.name) return `${seller.name} (${seller.id})`; - return seller.name || seller.id; + return formatDisplayName(removeTrailingSellerId(seller.name || seller.id)); }; const marketplaceOptions = useMemo(() => {