Improve planning data normalization
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m25s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m25s
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import type { CutFamilyKey, CutProductOverride, DateRange, ProductAnalyticsItem, ProductionOrderItem } from '../types';
|
import type { CutFamilyKey, CutProductOverride, DateRange, ProductAnalyticsItem, ProductionOrderItem } from '../types';
|
||||||
import { normalizeProductText, parseProductName, sortProductSizes } from '../productParsing.ts';
|
import { normalizeProductText, parseProductName, sortProductSizes } from '../productParsing.ts';
|
||||||
import { resolveProductType } from '../productClassification.ts';
|
import { resolveProductType } from '../productClassification.ts';
|
||||||
|
import { getPlanningStock } from '../planningStock.ts';
|
||||||
|
|
||||||
export type { CutFamilyKey, CutProductOverride };
|
export type { CutFamilyKey, CutProductOverride };
|
||||||
|
|
||||||
@@ -216,7 +217,7 @@ export const buildCutPlan = (
|
|||||||
const dailySales = product.quantitySold / rangeDays;
|
const dailySales = product.quantitySold / rangeDays;
|
||||||
const projectedDemand = dailySales * targetCoverageDays;
|
const projectedDemand = dailySales * targetCoverageDays;
|
||||||
const openProductionQuantity = openProductionByProductId[product.id] || 0;
|
const openProductionQuantity = openProductionByProductId[product.id] || 0;
|
||||||
const availableQuantity = product.stock + openProductionQuantity;
|
const availableQuantity = getPlanningStock(product.stock) + openProductionQuantity;
|
||||||
const suggestedCutQuantity = Math.max(0, Math.ceil(projectedDemand - availableQuantity));
|
const suggestedCutQuantity = Math.max(0, Math.ceil(projectedDemand - availableQuantity));
|
||||||
const estimatedRolls = family.unitsPerRoll && suggestedCutQuantity > 0
|
const estimatedRolls = family.unitsPerRoll && suggestedCutQuantity > 0
|
||||||
? Math.ceil(suggestedCutQuantity / family.unitsPerRoll)
|
? Math.ceil(suggestedCutQuantity / family.unitsPerRoll)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { parseProductName } from '../productParsing';
|
|||||||
import { formatColorLabel } from '../displayFormatters';
|
import { formatColorLabel } from '../displayFormatters';
|
||||||
import { averageRecentValues, formatDateBucketLabel, formatDateBucketLongLabel, getAutoDateBucket, getMovingAverageWindow, getRangeDayCount, getDateBucketKey, type DateBucket } from '../chartUtils';
|
import { averageRecentValues, formatDateBucketLabel, formatDateBucketLongLabel, getAutoDateBucket, getMovingAverageWindow, getRangeDayCount, getDateBucketKey, type DateBucket } from '../chartUtils';
|
||||||
import { getProductTypeConfig, resolveProductType } from '../productClassification';
|
import { getProductTypeConfig, resolveProductType } from '../productClassification';
|
||||||
|
import { getPlanningStock } from '../planningStock';
|
||||||
|
|
||||||
const CHART_GRID_COLOR = 'var(--chart-grid)';
|
const CHART_GRID_COLOR = 'var(--chart-grid)';
|
||||||
const CHART_AXIS_COLOR = 'var(--chart-axis)';
|
const CHART_AXIS_COLOR = 'var(--chart-axis)';
|
||||||
@@ -229,7 +230,7 @@ const ProductDetails = () => {
|
|||||||
const dateBucket = isHourlyChart ? 'day' : getAutoDateBucket(dateRange);
|
const dateBucket = isHourlyChart ? 'day' : getAutoDateBucket(dateRange);
|
||||||
const periodDays = getRangeDayCount(dateRange);
|
const periodDays = getRangeDayCount(dateRange);
|
||||||
const dailyAverageSold = totalSold / periodDays;
|
const dailyAverageSold = totalSold / periodDays;
|
||||||
const projectedStockDays = dailyAverageSold > 0 ? productInfo.stock / dailyAverageSold : null;
|
const projectedStockDays = dailyAverageSold > 0 ? getPlanningStock(productInfo.stock) / dailyAverageSold : null;
|
||||||
const stockActionLabel = projectedStockDays === null
|
const stockActionLabel = projectedStockDays === null
|
||||||
? 'Sem venda no período'
|
? 'Sem venda no período'
|
||||||
: projectedStockDays <= 7
|
: projectedStockDays <= 7
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { decodeProductGroupKey, normalizeProductText, parseProductName } from '.
|
|||||||
import { formatColorLabel } from '../displayFormatters';
|
import { formatColorLabel } from '../displayFormatters';
|
||||||
import { getProductColor } from '../productColors';
|
import { getProductColor } from '../productColors';
|
||||||
import { getDominantProductType, getProductTypeConfig, resolveProductType, type ProductTypeKey } from '../productClassification';
|
import { getDominantProductType, getProductTypeConfig, resolveProductType, type ProductTypeKey } from '../productClassification';
|
||||||
|
import { getPlanningStock } from '../planningStock';
|
||||||
import type { CuttingSettings, DateRange, ProductAnalyticsItem, ProductionOrderItem } from '../types';
|
import type { CuttingSettings, DateRange, ProductAnalyticsItem, ProductionOrderItem } from '../types';
|
||||||
|
|
||||||
type VariantRow = ProductAnalyticsItem & {
|
type VariantRow = ProductAnalyticsItem & {
|
||||||
@@ -263,7 +264,7 @@ const ProductGroupDetails = () => {
|
|||||||
const dailySales = product.quantitySold / rangeDays;
|
const dailySales = product.quantitySold / rangeDays;
|
||||||
const projectedDemand = dailySales * REPLENISHMENT_TARGET_DAYS;
|
const projectedDemand = dailySales * REPLENISHMENT_TARGET_DAYS;
|
||||||
const openProductionQuantity = openProductionByProductId[product.id] || 0;
|
const openProductionQuantity = openProductionByProductId[product.id] || 0;
|
||||||
const availableQuantity = product.stock + openProductionQuantity;
|
const availableQuantity = getPlanningStock(product.stock) + openProductionQuantity;
|
||||||
const suggestedReplenishment = Math.max(0, Math.ceil(projectedDemand - availableQuantity));
|
const suggestedReplenishment = Math.max(0, Math.ceil(projectedDemand - availableQuantity));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { exportToCSV, fetchCuttingSettings, fetchProductAnalytics, saveCuttingSe
|
|||||||
import { encodeProductGroupKey, parseProductName, sortProductSizes } from '../productParsing';
|
import { encodeProductGroupKey, parseProductName, sortProductSizes } from '../productParsing';
|
||||||
import { formatColorLabel } from '../displayFormatters';
|
import { formatColorLabel } from '../displayFormatters';
|
||||||
import { getDominantProductType, getProductTypeConfig, productTypeOptions, resolveProductType, type ProductTypeKey } from '../productClassification';
|
import { getDominantProductType, getProductTypeConfig, productTypeOptions, resolveProductType, type ProductTypeKey } from '../productClassification';
|
||||||
|
import { getPlanningStock } from '../planningStock';
|
||||||
|
|
||||||
type StockRisk = 'rupture' | 'critical' | 'attention' | 'monitor' | 'healthy' | 'no_sales';
|
type StockRisk = 'rupture' | 'critical' | 'attention' | 'monitor' | 'healthy' | 'no_sales';
|
||||||
type StockStatusFilter = 'all' | StockRisk;
|
type StockStatusFilter = 'all' | StockRisk;
|
||||||
@@ -262,7 +263,8 @@ const Products = () => {
|
|||||||
const normalizedSearch = searchTerm.trim().toLowerCase();
|
const normalizedSearch = searchTerm.trim().toLowerCase();
|
||||||
const skuRows = productAnalytics.map(product => {
|
const skuRows = productAnalytics.map(product => {
|
||||||
const dailySales = product.quantitySold / days;
|
const dailySales = product.quantitySold / days;
|
||||||
const risk = classifyStockRisk(product.stock, dailySales);
|
const planningStock = getPlanningStock(product.stock);
|
||||||
|
const risk = classifyStockRisk(planningStock, dailySales);
|
||||||
const style = riskStyles[risk];
|
const style = riskStyles[risk];
|
||||||
const metadata = parseProductName(product.name);
|
const metadata = parseProductName(product.name);
|
||||||
const productType = resolveProductType(product.name, planningSettings.productOverrides[product.id]);
|
const productType = resolveProductType(product.name, planningSettings.productOverrides[product.id]);
|
||||||
@@ -270,7 +272,7 @@ const Products = () => {
|
|||||||
return {
|
return {
|
||||||
...product,
|
...product,
|
||||||
dailySales,
|
dailySales,
|
||||||
daysOfCover: dailySales > 0 ? product.stock / dailySales : null,
|
daysOfCover: dailySales > 0 ? planningStock / dailySales : null,
|
||||||
risk,
|
risk,
|
||||||
riskLabel: style.label,
|
riskLabel: style.label,
|
||||||
baseName: metadata.baseName,
|
baseName: metadata.baseName,
|
||||||
@@ -302,10 +304,11 @@ const Products = () => {
|
|||||||
const quantitySold = group.reduce((total, product) => total + product.quantitySold, 0);
|
const quantitySold = group.reduce((total, product) => total + product.quantitySold, 0);
|
||||||
const revenue = group.reduce((total, product) => total + product.revenue, 0);
|
const revenue = group.reduce((total, product) => total + product.revenue, 0);
|
||||||
const stock = group.reduce((total, product) => total + product.stock, 0);
|
const stock = group.reduce((total, product) => total + product.stock, 0);
|
||||||
|
const planningStock = group.reduce((total, product) => total + getPlanningStock(product.stock), 0);
|
||||||
const orderLineCount = group.reduce((total, product) => total + product.orderLineCount, 0);
|
const orderLineCount = group.reduce((total, product) => total + product.orderLineCount, 0);
|
||||||
const dailySales = quantitySold / days;
|
const dailySales = quantitySold / days;
|
||||||
const daysOfCover = dailySales > 0 ? stock / dailySales : null;
|
const daysOfCover = dailySales > 0 ? planningStock / dailySales : null;
|
||||||
const risk = classifyStockRisk(stock, dailySales);
|
const risk = classifyStockRisk(planningStock, dailySales);
|
||||||
const style = riskStyles[risk];
|
const style = riskStyles[risk];
|
||||||
const colorTotals = new Map<string, number>();
|
const colorTotals = new Map<string, number>();
|
||||||
const sizeTotals = new Map<string, number>();
|
const sizeTotals = new Map<string, number>();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type { DateRange, ProductAnalyticsItem, ProductionOrderItem } from '../ty
|
|||||||
import { exportToCSV, fetchProductAnalytics, fetchProductionOrders } from '../dataService';
|
import { exportToCSV, fetchProductAnalytics, fetchProductionOrders } from '../dataService';
|
||||||
import { encodeProductGroupKey, parseProductName, sortProductSizes } from '../productParsing';
|
import { encodeProductGroupKey, parseProductName, sortProductSizes } from '../productParsing';
|
||||||
import { formatColorLabel } from '../displayFormatters';
|
import { formatColorLabel } from '../displayFormatters';
|
||||||
|
import { getPlanningStock } from '../planningStock';
|
||||||
|
|
||||||
type ReplenishmentStatus = 'need' | 'covered' | 'no_sales' | 'no_stock';
|
type ReplenishmentStatus = 'need' | 'covered' | 'no_sales' | 'no_stock';
|
||||||
type ReplenishmentFilter = 'all' | ReplenishmentStatus;
|
type ReplenishmentFilter = 'all' | ReplenishmentStatus;
|
||||||
@@ -174,7 +175,7 @@ const Replenishment = () => {
|
|||||||
const dailySales = product.quantitySold / rangeDays;
|
const dailySales = product.quantitySold / rangeDays;
|
||||||
const projectedDemand = dailySales * targetCoverageDays;
|
const projectedDemand = dailySales * targetCoverageDays;
|
||||||
const openProductionQuantity = openProductionByProductId[product.id] || 0;
|
const openProductionQuantity = openProductionByProductId[product.id] || 0;
|
||||||
const availableQuantity = product.stock + openProductionQuantity;
|
const availableQuantity = getPlanningStock(product.stock) + openProductionQuantity;
|
||||||
const rawNeed = projectedDemand - availableQuantity;
|
const rawNeed = projectedDemand - availableQuantity;
|
||||||
const suggestedQuantity = Math.max(0, Math.ceil(rawNeed));
|
const suggestedQuantity = Math.max(0, Math.ceil(rawNeed));
|
||||||
const daysOfCover = dailySales > 0 ? availableQuantity / dailySales : null;
|
const daysOfCover = dailySales > 0 ? availableQuantity / dailySales : null;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { classifyCutFamily } from '../analytics/cutting';
|
|||||||
import { adjustSupplyLotInventory, approveSupplyReceipt, consumeSupplyLotForProduction, createSupplyFabricPlan, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, fetchCuttingSettings, fetchProductAnalytics, fetchSupplySummary } from '../dataService';
|
import { adjustSupplyLotInventory, approveSupplyReceipt, consumeSupplyLotForProduction, createSupplyFabricPlan, createSupplyReceipt, deleteSupplyFabricPlan, deleteSupplyReceipt, fetchCuttingSettings, fetchProductAnalytics, fetchSupplySummary } from '../dataService';
|
||||||
import { parseProductName } from '../productParsing';
|
import { parseProductName } from '../productParsing';
|
||||||
import { resolveProductType, type ProductTypeKey } from '../productClassification';
|
import { resolveProductType, type ProductTypeKey } from '../productClassification';
|
||||||
|
import { getPlanningStock } from '../planningStock';
|
||||||
import type { CuttingSettings, DateRange, ProductAnalyticsItem, SupplyFabricPlan, SupplyLot, SupplyMovement, SupplyPurchaseNeed, SupplyReceipt, SupplySummary } from '../types';
|
import type { CuttingSettings, DateRange, ProductAnalyticsItem, SupplyFabricPlan, SupplyLot, SupplyMovement, SupplyPurchaseNeed, SupplyReceipt, SupplySummary } from '../types';
|
||||||
|
|
||||||
type InventoryTab = 'dashboard' | 'balance' | 'receipts' | 'inventory' | 'movements';
|
type InventoryTab = 'dashboard' | 'balance' | 'receipts' | 'inventory' | 'movements';
|
||||||
@@ -230,9 +231,10 @@ const buildDemandRow = (
|
|||||||
const color = override?.color || metadata.color;
|
const color = override?.color || metadata.color;
|
||||||
const size = (override?.size || metadata.size).toUpperCase();
|
const size = (override?.size || metadata.size).toUpperCase();
|
||||||
const dailySales = product.quantitySold / rangeDays;
|
const dailySales = product.quantitySold / rangeDays;
|
||||||
const daysOfCover = dailySales > 0 ? product.stock / dailySales : null;
|
const planningStock = getPlanningStock(product.stock);
|
||||||
|
const daysOfCover = dailySales > 0 ? planningStock / dailySales : null;
|
||||||
const targetDemand = dailySales * targetCoverageDays;
|
const targetDemand = dailySales * targetCoverageDays;
|
||||||
const suggestedUnits = Math.max(0, Math.ceil(targetDemand - product.stock));
|
const suggestedUnits = Math.max(0, Math.ceil(targetDemand - planningStock));
|
||||||
const missingData: string[] = [];
|
const missingData: string[] = [];
|
||||||
|
|
||||||
if (planningReviewTypes.has(productType)) missingData.push('tipo');
|
if (planningReviewTypes.has(productType)) missingData.push('tipo');
|
||||||
|
|||||||
1
src/planningStock.ts
Normal file
1
src/planningStock.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const getPlanningStock = (stock: number) => Math.max(0, stock);
|
||||||
@@ -123,17 +123,20 @@ export const classifyProductType = (name: string): ProductTypeKey => {
|
|||||||
|
|
||||||
if (has(normalizedName, /\bSALDO ESTOQUE\b/)) return 'ignore_from_planning';
|
if (has(normalizedName, /\bSALDO ESTOQUE\b/)) return 'ignore_from_planning';
|
||||||
if (has(normalizedName, /^(?:\d+\s+)?(?:MALHA|RIBANA)\b/)) return 'raw_material';
|
if (has(normalizedName, /^(?:\d+\s+)?(?:MALHA|RIBANA)\b/)) return 'raw_material';
|
||||||
if (has(normalizedName, /\b(?:RETALHO|RESIDUO)\s+(?:DE\s+)?MALHA\b/)) return 'raw_material';
|
if (has(normalizedName, /\b(?:RETALHO|RESIDUO)\s+(?:DE\s+)?(?:MALHA|MOLETOM)\b/)) return 'raw_material';
|
||||||
if (has(normalizedName, /\bFIO\b.*\bMALHARIA\b/)) return 'raw_material';
|
if (has(normalizedName, /\bFIO\b.*\bMALHARIA\b/)) return 'raw_material';
|
||||||
if (has(normalizedName, /\b(?:LINHA|FIO) PARA COSTURA\b/)) return 'raw_material';
|
if (has(normalizedName, /\b(?:LINHA|FIO) PARA COSTURA\b/)) return 'raw_material';
|
||||||
|
if (has(normalizedName, /\bATACADOR\b/)) return 'raw_material';
|
||||||
|
if (has(normalizedName, /\bILHOS\b/)) return 'raw_material';
|
||||||
if (has(normalizedName, /ETIQUETA|TAG|FITA/)) return 'raw_material';
|
if (has(normalizedName, /ETIQUETA|TAG|FITA/)) return 'raw_material';
|
||||||
|
if (has(normalizedName, /\bCREDITO\b/)) return 'ignore_from_planning';
|
||||||
if (has(normalizedName, /\bKIT\b/)) return 'kit_bundle';
|
if (has(normalizedName, /\bKIT\b/)) return 'kit_bundle';
|
||||||
if (has(normalizedName, /TRANSPARENTE PP|SACO DE SEGURANCA|MILHEIRO|SACOLA|EMBALAGEM/)) return 'packaging';
|
if (has(normalizedName, /TRANSPARENTE PP|SACO DE SEGURANCA|MILHEIRO|SACOLA|EMBALAGEM/)) return 'packaging';
|
||||||
if (has(normalizedName, /PRENSA|MAQUINA|OVERLOCK/)) return 'equipment';
|
if (has(normalizedName, /PRENSA|MAQUINA|OVERLOCK|OVERLOK|GALONEIRA|PRATELEIRA/)) return 'equipment';
|
||||||
if (has(normalizedName, /FRETE|SERVICO|TRANSPORTE|MOTOTAXI|TECELAGEM|TINTURARIA/)) return 'service';
|
if (has(normalizedName, /FRETE|SERVICO|TRANSPORTE|MOTOTAXI|TECELAGEM|TINTURARIA|MAO DE OBRA/)) return 'service';
|
||||||
if (has(normalizedName, /DUMPER|CABO FLAT|WIPPER|PRIMER|FLUIDO DE LIMPEZA|MISTURADOR|CABECA I3200|PECA DE MAQUINA/)) return 'machine_part';
|
if (has(normalizedName, /DUMPER|CABO FLAT|WIPPER|PRIMER|FLUIDO DE LIMPEZA|MISTURADOR|CABECA I3200|CABECA DE IMPRESSAO|SENSOR INFRAVERMELHO|CAPSULA FILTRO|PECA DE MAQUINA/)) return 'machine_part';
|
||||||
if (has(normalizedName, /TINTA DTF|FILME DTF|POLIAMIDA.*DTF|DTF ROLO|PO PARA DTF/)) return 'dtf_input';
|
if (has(normalizedName, /TINTA DTF|FILME DTF|POLIAMIDA.*DTF|DTF ROLO|PO PARA DTF|BOMBA DE TINTA/)) return 'dtf_input';
|
||||||
if (has(normalizedName, /IMPRESSAO DTF|CORRECAO IMPRESSAO|ESTAMPA|PERSONALIZACAO/)) return 'dtf_service';
|
if (has(normalizedName, /IMPRESSAO DTF|IMPRESSAO UV|CORRECAO IMPRESSAO|ESTAMPA|PERSONALIZACAO/)) return 'dtf_service';
|
||||||
if (has(normalizedName, /MALHA|RIBANA|FIO|TECIDO|RESIDUO|RETALHO|PIMA|ALGODAO/)) {
|
if (has(normalizedName, /MALHA|RIBANA|FIO|TECIDO|RESIDUO|RETALHO|PIMA|ALGODAO/)) {
|
||||||
if (!has(normalizedName, /CAMISETA|MOLETOM|REGATA|BONE|CHINELO|OVERSIZE|OVER SIZE/)) {
|
if (!has(normalizedName, /CAMISETA|MOLETOM|REGATA|BONE|CHINELO|OVERSIZE|OVER SIZE/)) {
|
||||||
return 'raw_material';
|
return 'raw_material';
|
||||||
|
|||||||
@@ -1,8 +1,54 @@
|
|||||||
const sizeOrder = ['2', '4', '6', '8', '10', '12', '14', '16', 'PP', 'P', 'M', 'G', 'GG', 'XG', 'G1', 'G2', 'G3', 'G4', 'G5'];
|
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);
|
const sizeSet = new Set(sizeOrder);
|
||||||
|
const knownColors = [
|
||||||
|
'BRANCO + PRETO',
|
||||||
|
'CINZA/PRETO',
|
||||||
|
'VERDE BANDEIRA',
|
||||||
|
'VERDE MILITAR',
|
||||||
|
'AZUL MARINHO',
|
||||||
|
'CINZA GRAFITE',
|
||||||
|
'AMARELO',
|
||||||
|
'BRANCO',
|
||||||
|
'GRAFITE',
|
||||||
|
'MARINHO',
|
||||||
|
'VERMELHO',
|
||||||
|
'BORDO',
|
||||||
|
'CAFE',
|
||||||
|
'CAQUI',
|
||||||
|
'CHUMBO',
|
||||||
|
'CINZA',
|
||||||
|
'MARROM',
|
||||||
|
'PEROLA',
|
||||||
|
'PRETO',
|
||||||
|
'ROYAL',
|
||||||
|
'ROSA',
|
||||||
|
'ROXO',
|
||||||
|
'VERDE',
|
||||||
|
'AZUL',
|
||||||
|
'BEGE'
|
||||||
|
].sort((a, b) => b.length - a.length);
|
||||||
|
|
||||||
export const normalizeProductText = (value: string) => value.replace(/\s+/g, ' ').trim();
|
export const normalizeProductText = (value: string) => value.replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
|
const normalizeForMatch = (value: string) => normalizeProductText(value)
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/\p{Diacritic}/gu, '')
|
||||||
|
.toUpperCase();
|
||||||
|
|
||||||
|
const escapeRegex = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
|
||||||
|
const toFlexiblePattern = (value: string) => escapeRegex(value).replace(/\s+/g, '\\s+');
|
||||||
|
|
||||||
|
const findLeadingColor = (value: string) => {
|
||||||
|
const normalizedValue = normalizeForMatch(value);
|
||||||
|
return knownColors.find(color => normalizedValue === color || normalizedValue.startsWith(`${color} `)) || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const findTrailingColor = (value: string) => {
|
||||||
|
const normalizedValue = normalizeForMatch(value);
|
||||||
|
return knownColors.find(color => normalizedValue === color || normalizedValue.endsWith(` ${color}`) || normalizedValue.endsWith(`- ${color}`)) || '';
|
||||||
|
};
|
||||||
|
|
||||||
export const encodeProductGroupKey = (value: string) => {
|
export const encodeProductGroupKey = (value: string) => {
|
||||||
const normalizedValue = normalizeProductText(value);
|
const normalizedValue = normalizeProductText(value);
|
||||||
const bytes = new TextEncoder().encode(normalizedValue);
|
const bytes = new TextEncoder().encode(normalizedValue);
|
||||||
@@ -34,16 +80,51 @@ export const parseProductName = (name: string) => {
|
|||||||
const explicitSizeMatch = cleanName.match(/\bTAMANHO\s*-?\s*([A-Z0-9]+)\b/i);
|
const explicitSizeMatch = cleanName.match(/\bTAMANHO\s*-?\s*([A-Z0-9]+)\b/i);
|
||||||
const trailingTokenMatch = cleanName.match(/(?:\s+-\s+|\s)([A-Z0-9]+)$/i);
|
const trailingTokenMatch = cleanName.match(/(?:\s+-\s+|\s)([A-Z0-9]+)$/i);
|
||||||
const trailingToken = trailingTokenMatch?.[1]?.toUpperCase() || '';
|
const trailingToken = trailingTokenMatch?.[1]?.toUpperCase() || '';
|
||||||
const size = (explicitSizeMatch?.[1] || (sizeSet.has(trailingToken) ? trailingToken : '')).toUpperCase();
|
let size = (explicitSizeMatch?.[1] || (sizeSet.has(trailingToken) ? trailingToken : '')).toUpperCase();
|
||||||
|
|
||||||
const colorMatch = cleanName.match(/\bCOR\s+(.+?)(?:\s+TAMANHO|\s+-\s+[A-Z0-9]+$|$)/i);
|
const colorMatch = cleanName.match(/\bCOR\s+(.+?)(?:\s+TAMANHO|\s+-\s+[A-Z0-9]+$|$)/i);
|
||||||
const color = normalizeProductText(colorMatch?.[1] || '');
|
let color = normalizeProductText(findLeadingColor(colorMatch?.[1] || '') || colorMatch?.[1] || '');
|
||||||
|
|
||||||
|
if (!color && size) {
|
||||||
|
const nameBeforeSize = cleanName.replace(new RegExp(`(?:\\s+-\\s+|\\s)${escapeRegex(size)}$`, 'i'), '');
|
||||||
|
color = findTrailingColor(nameBeforeSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!color) {
|
||||||
|
const trailingColor = findTrailingColor(cleanName);
|
||||||
|
if (trailingColor) {
|
||||||
|
const normalizedName = normalizeForMatch(cleanName);
|
||||||
|
const normalizedPrefix = normalizeProductText(normalizedName.replace(new RegExp(`${escapeRegex(trailingColor)}$`), ''));
|
||||||
|
const tokenBeforeColor = normalizedPrefix.match(/([A-Z0-9]+)$/)?.[1] || '';
|
||||||
|
if (sizeSet.has(tokenBeforeColor)) {
|
||||||
|
color = trailingColor;
|
||||||
|
size = tokenBeforeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let baseName = cleanName
|
let baseName = cleanName
|
||||||
.replace(/\bCOR\s+.+?(?:\s+TAMANHO\s*-?\s*[A-Z0-9]+|\s+-\s+[A-Z0-9]+$|$)/i, '')
|
.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(/\bTAMANHO\s*-?\s*[A-Z0-9]+\b/i, '')
|
||||||
.replace(/\s+-\s*[A-Z0-9]+$/i, '');
|
.replace(/\s+-\s*[A-Z0-9]+$/i, '');
|
||||||
|
|
||||||
|
if (color && size) {
|
||||||
|
const colorPattern = toFlexiblePattern(color);
|
||||||
|
const sizePattern = escapeRegex(size);
|
||||||
|
baseName = baseName
|
||||||
|
.replace(new RegExp(`\\s+-\\s*${colorPattern}\\s+${sizePattern}$`, 'i'), '')
|
||||||
|
.replace(new RegExp(`\\s+${colorPattern}\\s+-\\s*${sizePattern}$`, 'i'), '')
|
||||||
|
.replace(new RegExp(`\\s+${sizePattern}\\s+${colorPattern}$`, 'i'), '')
|
||||||
|
.replace(new RegExp(`\\s+${colorPattern}\\s+${sizePattern}$`, 'i'), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (color) {
|
||||||
|
const colorPattern = toFlexiblePattern(color);
|
||||||
|
baseName = baseName
|
||||||
|
.replace(new RegExp(`\\s+-\\s*${colorPattern}$`, 'i'), '')
|
||||||
|
.replace(new RegExp(`\\s+${colorPattern}$`, 'i'), '');
|
||||||
|
}
|
||||||
|
|
||||||
baseName = normalizeProductText(baseName.replace(/\s+-\s*$/g, ''));
|
baseName = normalizeProductText(baseName.replace(/\s+-\s*$/g, ''));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user