From 56cff262abbff82a2e849cd17b104202b9d36e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Tue, 7 Jul 2026 09:59:07 -0300 Subject: [PATCH] Add grouped replenishment analysis --- src/pages/ProductDetails.tsx | 16 +++- src/pages/Products.tsx | 11 ++- src/pages/Replenishment.tsx | 165 +++++++++++++++++++++++++++++------ src/productParsing.ts | 37 ++++++++ 4 files changed, 199 insertions(+), 30 deletions(-) create mode 100644 src/productParsing.ts diff --git a/src/pages/ProductDetails.tsx b/src/pages/ProductDetails.tsx index 0ddc68d..159f35f 100644 --- a/src/pages/ProductDetails.tsx +++ b/src/pages/ProductDetails.tsx @@ -6,6 +6,7 @@ import DateRangePicker from '../components/DateRangePicker'; import RefreshStatus from '../components/RefreshStatus'; import type { DateRange, ProductDetailsAnalytics } from '../types'; import { fetchProductDetailsAnalytics } from '../dataService'; +import { parseProductName } from '../productParsing'; const CHART_GRID_COLOR = 'var(--chart-grid)'; const CHART_AXIS_COLOR = 'var(--chart-axis)'; @@ -351,13 +352,26 @@ const ProductDetails = () => {
{variantBreakdown.map(variant => { const width = maxVariantQuantity ? Math.max(4, (variant.quantitySold / maxVariantQuantity) * 100) : 0; + const metadata = parseProductName(variant.name); return (
{variant.name}
-
#{variant.id}
+
+ #{variant.id} + {metadata.color && ( + + {metadata.color} + + )} + {metadata.size && ( + + Tam. {metadata.size} + + )} +
diff --git a/src/pages/Products.tsx b/src/pages/Products.tsx index caae4e8..cd07950 100644 --- a/src/pages/Products.tsx +++ b/src/pages/Products.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { Link, useOutletContext } from 'react-router-dom'; -import { Download, Filter, Package, PackageCheck, Search, TrendingDown, TrendingUp, X } from 'lucide-react'; +import { Download, Filter, Package, PackageCheck, PackagePlus, Search, TrendingDown, TrendingUp, X } from 'lucide-react'; import PaginationControls from '../components/PaginationControls'; import RefreshStatus from '../components/RefreshStatus'; import type { DateRange, ProductAnalyticsItem } from '../types'; @@ -569,6 +569,15 @@ const Products = () => { )}
+ + + Reposição + + + ))} +
+
{ ) : (
- +
- + - + - - + + @@ -395,10 +501,13 @@ const Replenishment = () => { return ( - + @@ -421,7 +530,7 @@ const Replenishment = () => { to={`/products/${row.id}`} className="inline-flex items-center whitespace-nowrap text-xs font-bold text-brand-primary hover:opacity-80 transition-opacity bg-brand-primary/10 px-3 py-1.5 rounded-lg cursor-pointer" > - Ver produto + {viewMode === 'group' ? 'Ver líder' : 'Ver produto'} diff --git a/src/productParsing.ts b/src/productParsing.ts new file mode 100644 index 0000000..97c0117 --- /dev/null +++ b/src/productParsing.ts @@ -0,0 +1,37 @@ +const sizeOrder = ['2', '4', '6', '8', '10', '12', '14', '16', 'PP', 'P', 'M', 'G', 'GG', 'XG', 'G1', 'G2', 'G3', 'G4', 'G5']; +const sizeSet = new Set(sizeOrder); + +export const normalizeProductText = (value: string) => value.replace(/\s+/g, ' ').trim(); + +export const sortProductSizes = (sizes: string[]) => [...sizes].sort((a, b) => { + const indexA = sizeOrder.indexOf(a); + const indexB = sizeOrder.indexOf(b); + if (indexA !== -1 || indexB !== -1) { + return (indexA === -1 ? Number.MAX_SAFE_INTEGER : indexA) - (indexB === -1 ? Number.MAX_SAFE_INTEGER : indexB); + } + return a.localeCompare(b, 'pt-BR'); +}); + +export const parseProductName = (name: string) => { + const cleanName = normalizeProductText(name); + const explicitSizeMatch = cleanName.match(/\bTAMANHO\s*-?\s*([A-Z0-9]+)\b/i); + const trailingTokenMatch = cleanName.match(/(?:\s+-\s+|\s)([A-Z0-9]+)$/i); + const trailingToken = trailingTokenMatch?.[1]?.toUpperCase() || ''; + const size = (explicitSizeMatch?.[1] || (sizeSet.has(trailingToken) ? trailingToken : '')).toUpperCase(); + + const colorMatch = cleanName.match(/\bCOR\s+(.+?)(?:\s+TAMANHO|\s+-\s+[A-Z0-9]+$|$)/i); + const color = normalizeProductText(colorMatch?.[1] || ''); + + let baseName = cleanName + .replace(/\bCOR\s+.+?(?:\s+TAMANHO\s*-?\s*[A-Z0-9]+|\s+-\s+[A-Z0-9]+$|$)/i, '') + .replace(/\bTAMANHO\s*-?\s*[A-Z0-9]+\b/i, '') + .replace(/\s+-\s*[A-Z0-9]+$/i, ''); + + baseName = normalizeProductText(baseName.replace(/\s+-\s*$/g, '')); + + return { + baseName: baseName || cleanName, + color, + size + }; +};
ID ProdutoDescrição{viewMode === 'group' ? 'SKUs' : 'ID Produto'}{viewMode === 'group' ? 'Grupo / cor' : 'Descrição'} Status Demanda proj. Estoque
#{row.id} + {viewMode === 'group' ? `${row.productCount} SKUs` : `#${row.id}`} +
{row.name}
+ {viewMode === 'group' && row.sizes.length ? `Tamanhos: ${row.sizes.join(', ')} · ` : ''} Média: {formatNumber(row.dailySales, 2)} un./dia · Vendido: {formatNumber(row.quantitySold)} un.