Format product color labels

This commit is contained in:
Cauê Faleiros
2026-07-09 13:58:48 -03:00
parent 634cf4db48
commit d3d8886a90
7 changed files with 57 additions and 33 deletions

View File

@@ -1,29 +1,5 @@
const COLOR_SWATCHES: Array<{ pattern: string; color: string }> = [
{ pattern: 'preto', color: '#171717' },
{ pattern: 'branco', color: '#f8fafc' },
{ pattern: 'bege', color: '#d7bf9a' },
{ pattern: 'cafe', color: '#79553d' },
{ pattern: 'perola', color: '#e7dfcf' },
{ pattern: 'marinho', color: '#172554' },
{ pattern: 'bordo', color: '#6b1226' },
{ pattern: 'verde', color: '#166534' },
{ pattern: 'rosa', color: '#f0a6bf' },
{ pattern: 'cinza', color: '#8f8f8f' },
{ pattern: 'vermelho', color: '#b91c1c' },
{ pattern: 'grafite', color: '#3f3f46' },
{ pattern: 'azul', color: '#2563eb' },
{ pattern: 'marron', color: '#6b4f3b' },
{ pattern: 'marrom', color: '#6b4f3b' }
];
const normalizeColorLabel = (label: string) => (
label.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase()
);
export const getProductColor = (label: string) => {
const normalizedLabel = normalizeColorLabel(label);
return COLOR_SWATCHES.find(item => normalizedLabel.includes(item.pattern))?.color || '#64748b';
};
import { formatColorLabel } from '../displayFormatters';
import { getProductColor } from '../productColors';
export const ProductColorSwatch = ({
label,
@@ -50,7 +26,8 @@ const ProductColorBadge = ({
emptyLabel?: string;
className?: string;
}) => {
const displayLabel = label?.trim() || emptyLabel;
const normalizedLabel = label?.trim();
const displayLabel = normalizedLabel ? formatColorLabel(normalizedLabel) : emptyLabel;
return (
<span className={`inline-flex max-w-full items-center gap-2 rounded-full border border-dark-border bg-dark-input px-2.5 py-1 text-xs font-bold text-dark-text ${className}`}>

View File

@@ -24,3 +24,18 @@ export const formatDisplayName = (value: string) => {
})
.join(' ');
};
export const formatColorLabel = (value: string) => {
const normalized = String(value || '').replace(/\s+/g, ' ').trim();
if (!normalized) return '';
if (normalized.toLocaleLowerCase('pt-BR') === 'sem cor') return 'Sem cor';
return normalized
.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(' ');
};

View File

@@ -8,6 +8,7 @@ import RefreshStatus from '../components/RefreshStatus';
import type { DateRange, ProductDetailsAnalytics } from '../types';
import { fetchProductDetailsAnalytics } from '../dataService';
import { parseProductName } from '../productParsing';
import { formatColorLabel } from '../displayFormatters';
const CHART_GRID_COLOR = 'var(--chart-grid)';
const CHART_AXIS_COLOR = 'var(--chart-axis)';
@@ -361,7 +362,7 @@ const ProductDetails = () => {
<span>#{variant.id}</span>
{metadata.color && (
<span className="rounded-full border border-sky-400/25 bg-sky-400/10 px-2 py-0.5 font-bold text-sky-300">
{metadata.color}
{formatColorLabel(metadata.color)}
</span>
)}
{metadata.size && (

View File

@@ -4,11 +4,13 @@ import { DollarSign, Package, Palette, Ruler, TrendingDown, TrendingUp, Warehous
import BackButton from '../components/BackButton';
import DateRangePicker from '../components/DateRangePicker';
import PaginationControls from '../components/PaginationControls';
import ProductColorBadge, { ProductColorSwatch, getProductColor } from '../components/ProductColorBadge';
import ProductColorBadge, { ProductColorSwatch } from '../components/ProductColorBadge';
import RefreshStatus from '../components/RefreshStatus';
import { buildOpenProductionByProductId } from '../analytics/cutting';
import { fetchProductAnalytics, fetchProductionOrders } from '../dataService';
import { decodeProductGroupKey, normalizeProductText, parseProductName } from '../productParsing';
import { formatColorLabel } from '../displayFormatters';
import { getProductColor } from '../productColors';
import type { DateRange, ProductAnalyticsItem, ProductionOrderItem } from '../types';
type VariantRow = ProductAnalyticsItem & {
@@ -121,13 +123,14 @@ const BreakdownPanel = ({
{visibleRows.map(row => {
const width = maxSold ? Math.max(4, (row.quantitySold / maxSold) * 100) : 0;
const barColor = type === 'color' ? getBarColor(row.label) : '#25c2ff';
const displayLabel = type === 'color' ? formatColorLabel(row.label) : row.label;
return (
<div key={row.label} className="grid grid-cols-[minmax(7.5rem,9rem)_1fr_6rem] items-center gap-3">
<div className="flex min-w-0 items-center gap-2">
{type === 'color' ? <ProductColorSwatch label={row.label} /> : <span className="h-2.5 w-2.5 shrink-0 rounded-sm bg-sky-400" />}
<span className="truncate text-xs font-bold text-dark-text" title={row.label}>
{type === 'size' && row.label !== 'Sem tamanho' ? `Tam. ${row.label}` : row.label}
<span className="truncate text-xs font-bold text-dark-text" title={displayLabel}>
{type === 'size' && row.label !== 'Sem tamanho' ? `Tam. ${row.label}` : displayLabel}
</span>
</div>
<div className="h-3 overflow-hidden rounded-full border border-dark-border bg-dark-input">

View File

@@ -7,6 +7,7 @@ import RefreshStatus from '../components/RefreshStatus';
import type { DateRange, ProductAnalyticsItem } from '../types';
import { exportToCSV, fetchProductAnalytics } from '../dataService';
import { encodeProductGroupKey, parseProductName, sortProductSizes } from '../productParsing';
import { formatColorLabel } from '../displayFormatters';
type StockRisk = 'rupture' | 'critical' | 'attention' | 'monitor' | 'healthy' | 'no_sales';
type StockStatusFilter = 'all' | StockRisk;
@@ -657,7 +658,7 @@ const Products = () => {
<div className="truncate font-semibold text-zinc-900 dark:text-dark-text" title={product.name}>{product.name}</div>
<div className="truncate text-[10px] text-zinc-400 dark:text-dark-muted font-medium">
{viewMode === 'group'
? `Cor principal: ${product.topColor} · Tam. principal: ${product.topSize} · ${product.colors.length} cores · ${product.sizes.length} tamanhos`
? `Cor principal: ${formatColorLabel(product.topColor)} · Tam. principal: ${product.topSize} · ${product.colors.length} cores · ${product.sizes.length} tamanhos`
: `Preço Atual: ${formatCurrency(product.lastPrice)}`}
</div>
</td>

View File

@@ -8,6 +8,7 @@ import { buildOpenProductionByProductId } from '../analytics/cutting';
import type { DateRange, ProductAnalyticsItem, ProductionOrderItem } from '../types';
import { exportToCSV, fetchProductAnalytics, fetchProductionOrders } from '../dataService';
import { encodeProductGroupKey, parseProductName, sortProductSizes } from '../productParsing';
import { formatColorLabel } from '../displayFormatters';
type ReplenishmentStatus = 'need' | 'covered' | 'no_sales' | 'no_stock';
type ReplenishmentFilter = 'all' | ReplenishmentStatus;
@@ -237,7 +238,7 @@ const Replenishment = () => {
: 'covered';
const sizes = sortProductSizes(Array.from(new Set(group.flatMap(row => row.sizes))));
const productIds = group.map(row => row.id);
const name = first.color ? `${first.baseName} · ${first.color}` : first.baseName;
const name = first.color ? `${first.baseName} · ${formatColorLabel(first.color)}` : first.baseName;
return {
...first,

26
src/productColors.ts Normal file
View File

@@ -0,0 +1,26 @@
const COLOR_SWATCHES: Array<{ pattern: string; color: string }> = [
{ pattern: 'preto', color: '#171717' },
{ pattern: 'branco', color: '#f8fafc' },
{ pattern: 'bege', color: '#d7bf9a' },
{ pattern: 'cafe', color: '#79553d' },
{ pattern: 'perola', color: '#e7dfcf' },
{ pattern: 'marinho', color: '#172554' },
{ pattern: 'bordo', color: '#6b1226' },
{ pattern: 'verde', color: '#166534' },
{ pattern: 'rosa', color: '#f0a6bf' },
{ pattern: 'cinza', color: '#8f8f8f' },
{ pattern: 'vermelho', color: '#b91c1c' },
{ pattern: 'grafite', color: '#3f3f46' },
{ pattern: 'azul', color: '#2563eb' },
{ pattern: 'marron', color: '#6b4f3b' },
{ pattern: 'marrom', color: '#6b4f3b' }
];
const normalizeColorLabel = (label: string) => (
label.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase()
);
export const getProductColor = (label: string) => {
const normalizedLabel = normalizeColorLabel(label);
return COLOR_SWATCHES.find(item => normalizedLabel.includes(item.pattern))?.color || '#64748b';
};