Add inferred product classification
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m36s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m36s
This commit is contained in:
158
src/productClassification.ts
Normal file
158
src/productClassification.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { normalizeProductText } from './productParsing.ts';
|
||||
|
||||
export type ProductTypeKey =
|
||||
| 'finished_apparel'
|
||||
| 'finished_accessory'
|
||||
| 'raw_material'
|
||||
| 'packaging'
|
||||
| 'dtf_input'
|
||||
| 'dtf_service'
|
||||
| 'kit_bundle'
|
||||
| 'service'
|
||||
| 'machine_part'
|
||||
| 'equipment'
|
||||
| 'ignore_from_planning'
|
||||
| 'unknown';
|
||||
|
||||
export type ProductPlanningMode = 'cutting' | 'unit_replenishment' | 'material' | 'service' | 'ignore' | 'review';
|
||||
|
||||
export type ProductTypeConfig = {
|
||||
key: ProductTypeKey;
|
||||
label: string;
|
||||
description: string;
|
||||
planningMode: ProductPlanningMode;
|
||||
badgeClassName: string;
|
||||
};
|
||||
|
||||
export const productTypeConfigs: Record<ProductTypeKey, ProductTypeConfig> = {
|
||||
finished_apparel: {
|
||||
key: 'finished_apparel',
|
||||
label: 'Vestuário',
|
||||
description: 'Produto acabado que pode entrar em corte/reposição por SKU.',
|
||||
planningMode: 'cutting',
|
||||
badgeClassName: 'border-sky-400/30 bg-sky-400/10 text-sky-300'
|
||||
},
|
||||
finished_accessory: {
|
||||
key: 'finished_accessory',
|
||||
label: 'Acessório',
|
||||
description: 'Produto acabado sem regra de malha principal.',
|
||||
planningMode: 'unit_replenishment',
|
||||
badgeClassName: 'border-cyan-400/30 bg-cyan-400/10 text-cyan-300'
|
||||
},
|
||||
raw_material: {
|
||||
key: 'raw_material',
|
||||
label: 'Matéria-prima',
|
||||
description: 'Entrada de produção, como malha, ribana, tecido, fio ou resíduo.',
|
||||
planningMode: 'material',
|
||||
badgeClassName: 'border-emerald-400/30 bg-emerald-400/10 text-emerald-300'
|
||||
},
|
||||
packaging: {
|
||||
key: 'packaging',
|
||||
label: 'Embalagem',
|
||||
description: 'Insumo de embalagem controlado por unidade/milheiro.',
|
||||
planningMode: 'unit_replenishment',
|
||||
badgeClassName: 'border-amber-400/30 bg-amber-400/10 text-amber-300'
|
||||
},
|
||||
dtf_input: {
|
||||
key: 'dtf_input',
|
||||
label: 'Insumo DTF',
|
||||
description: 'Insumo para DTF, como tinta, filme, poliamida ou fluido.',
|
||||
planningMode: 'material',
|
||||
badgeClassName: 'border-fuchsia-400/30 bg-fuchsia-400/10 text-fuchsia-300'
|
||||
},
|
||||
dtf_service: {
|
||||
key: 'dtf_service',
|
||||
label: 'Serviço DTF',
|
||||
description: 'Impressão, estampa ou personalização vendida como serviço/produto customizado.',
|
||||
planningMode: 'service',
|
||||
badgeClassName: 'border-purple-400/30 bg-purple-400/10 text-purple-300'
|
||||
},
|
||||
kit_bundle: {
|
||||
key: 'kit_bundle',
|
||||
label: 'Kit',
|
||||
description: 'Bundle marketplace composto por outros SKUs.',
|
||||
planningMode: 'review',
|
||||
badgeClassName: 'border-indigo-400/30 bg-indigo-400/10 text-indigo-300'
|
||||
},
|
||||
service: {
|
||||
key: 'service',
|
||||
label: 'Serviço',
|
||||
description: 'Frete, transporte, tecelagem, tinturaria ou serviço técnico.',
|
||||
planningMode: 'service',
|
||||
badgeClassName: 'border-zinc-400/30 bg-zinc-400/10 text-zinc-300'
|
||||
},
|
||||
machine_part: {
|
||||
key: 'machine_part',
|
||||
label: 'Peça máquina',
|
||||
description: 'Peça, limpeza ou manutenção de máquina.',
|
||||
planningMode: 'unit_replenishment',
|
||||
badgeClassName: 'border-orange-400/30 bg-orange-400/10 text-orange-300'
|
||||
},
|
||||
equipment: {
|
||||
key: 'equipment',
|
||||
label: 'Equipamento',
|
||||
description: 'Máquina ou equipamento permanente.',
|
||||
planningMode: 'ignore',
|
||||
badgeClassName: 'border-slate-400/30 bg-slate-400/10 text-slate-300'
|
||||
},
|
||||
ignore_from_planning: {
|
||||
key: 'ignore_from_planning',
|
||||
label: 'Ignorar',
|
||||
description: 'Item que não deve dirigir corte, compra ou reposição.',
|
||||
planningMode: 'ignore',
|
||||
badgeClassName: 'border-dark-border bg-dark-input text-dark-muted'
|
||||
},
|
||||
unknown: {
|
||||
key: 'unknown',
|
||||
label: 'Revisar',
|
||||
description: 'Tipo não identificado automaticamente.',
|
||||
planningMode: 'review',
|
||||
badgeClassName: 'border-red-400/30 bg-red-400/10 text-red-300'
|
||||
}
|
||||
};
|
||||
|
||||
const has = (value: string, pattern: RegExp) => pattern.test(value);
|
||||
|
||||
export const classifyProductType = (name: string): ProductTypeKey => {
|
||||
const normalizedName = normalizeProductText(name)
|
||||
.normalize('NFD')
|
||||
.replace(/\p{Diacritic}/gu, '')
|
||||
.toUpperCase();
|
||||
|
||||
if (!normalizedName) return 'unknown';
|
||||
|
||||
if (has(normalizedName, /\bKIT\b/)) return 'kit_bundle';
|
||||
if (has(normalizedName, /TRANSPARENTE PP|SACO DE SEGURANCA|MILHEIRO|SACOLA|EMBALAGEM/)) return 'packaging';
|
||||
if (has(normalizedName, /PRENSA|MAQUINA|OVERLOCK/)) return 'equipment';
|
||||
if (has(normalizedName, /FRETE|SERVICO|TRANSPORTE|MOTOTAXI|TECELAGEM|TINTURARIA/)) 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, /TINTA DTF|FILME DTF|POLIAMIDA.*DTF|DTF ROLO|PO PARA DTF/)) return 'dtf_input';
|
||||
if (has(normalizedName, /IMPRESSAO DTF|CORRECAO IMPRESSAO|ESTAMPA|PERSONALIZACAO/)) return 'dtf_service';
|
||||
if (has(normalizedName, /MALHA|RIBANA|FIO|TECIDO|RESIDUO|RETALHO|PIMA|ALGODAO/)) {
|
||||
if (!has(normalizedName, /CAMISETA|MOLETOM|REGATA|BONE|CHINELO|OVERSIZE|OVER SIZE/)) {
|
||||
return 'raw_material';
|
||||
}
|
||||
}
|
||||
if (has(normalizedName, /BONE|TRUCKER|\bCAP\b|CHINELO/)) return 'finished_accessory';
|
||||
if (has(normalizedName, /CAMISETA|MOLETOM|CANGURU|REGATA|INFANTIL|OVER SIZE|OVERSIZE|PROMOCIONAL|VESTUARIO/)) return 'finished_apparel';
|
||||
if (has(normalizedName, /ETIQUETA|TAG|FITA|LINHA PARA COSTURA|COSTURA/)) return 'raw_material';
|
||||
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
export const getProductTypeConfig = (type: ProductTypeKey) => productTypeConfigs[type] || productTypeConfigs.unknown;
|
||||
|
||||
export const productTypeOptions = Object.values(productTypeConfigs)
|
||||
.filter(config => config.key !== 'ignore_from_planning')
|
||||
.map(config => ({ value: config.key, label: config.label }));
|
||||
|
||||
export const getDominantProductType = <T extends { productType: ProductTypeKey; quantitySold: number }>(rows: T[]) => {
|
||||
if (!rows.length) return 'unknown';
|
||||
|
||||
const totals = rows.reduce<Record<string, number>>((acc, row) => {
|
||||
acc[row.productType] = (acc[row.productType] || 0) + Math.max(row.quantitySold, 1);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return (Object.entries(totals).sort((a, b) => b[1] - a[1])[0]?.[0] || 'unknown') as ProductTypeKey;
|
||||
};
|
||||
Reference in New Issue
Block a user