Add inferred product classification
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m36s

This commit is contained in:
Cauê Faleiros
2026-07-16 10:15:24 -03:00
parent 5e81a8f2c3
commit 422828f343
7 changed files with 274 additions and 16 deletions

View File

@@ -5,6 +5,7 @@ import BackButton from '../components/BackButton';
import DateRangePicker from '../components/DateRangePicker';
import PaginationControls from '../components/PaginationControls';
import ProductColorBadge, { ProductColorSwatch } from '../components/ProductColorBadge';
import ProductTypeBadge from '../components/ProductTypeBadge';
import RefreshStatus from '../components/RefreshStatus';
import { buildSkuEditPath } from '../catalogLinks';
import { buildOpenProductionByProductId } from '../analytics/cutting';
@@ -12,6 +13,7 @@ import { fetchProductAnalytics, fetchProductionOrders } from '../dataService';
import { decodeProductGroupKey, normalizeProductText, parseProductName } from '../productParsing';
import { formatColorLabel } from '../displayFormatters';
import { getProductColor } from '../productColors';
import { classifyProductType, getDominantProductType, getProductTypeConfig, type ProductTypeKey } from '../productClassification';
import type { DateRange, ProductAnalyticsItem, ProductionOrderItem } from '../types';
type VariantRow = ProductAnalyticsItem & {
@@ -23,6 +25,7 @@ type VariantRow = ProductAnalyticsItem & {
availableQuantity: number;
suggestedReplenishment: number;
daysOfCover: number | null;
productType: ProductTypeKey;
};
type BreakdownRow = {
@@ -240,6 +243,7 @@ const ProductGroupDetails = () => {
return products
.map(product => {
const metadata = parseProductName(product.name);
const productType = classifyProductType(product.name);
const dailySales = product.quantitySold / rangeDays;
const projectedDemand = dailySales * REPLENISHMENT_TARGET_DAYS;
const openProductionQuantity = openProductionByProductId[product.id] || 0;
@@ -256,7 +260,8 @@ const ProductGroupDetails = () => {
openProductionQuantity,
availableQuantity,
suggestedReplenishment,
daysOfCover: dailySales > 0 ? availableQuantity / dailySales : null
daysOfCover: dailySales > 0 ? availableQuantity / dailySales : null,
productType
};
})
.filter(product => normalizeProductText(product.baseName).toLowerCase() === normalizedGroupName)
@@ -275,6 +280,7 @@ const ProductGroupDetails = () => {
const daysOfCover = dailySales > 0 ? availableQuantity / dailySales : null;
const colors = new Set(groupRows.map(row => row.color).filter(Boolean));
const sizes = new Set(groupRows.map(row => row.size).filter(Boolean));
const productType = getDominantProductType(groupRows);
return {
totalSold,
@@ -287,9 +293,11 @@ const ProductGroupDetails = () => {
suggestedReplenishment,
daysOfCover,
colorCount: colors.size,
sizeCount: sizes.size
sizeCount: sizes.size,
productType
};
}, [groupRows]);
const productTypeConfig = getProductTypeConfig(totals.productType);
const colorBreakdown = useMemo(() => buildBreakdown(groupRows, 'color'), [groupRows]);
const sizeBreakdown = useMemo(() => buildBreakdown(groupRows, 'size'), [groupRows]);
@@ -335,6 +343,10 @@ const ProductGroupDetails = () => {
Grupo · {formatNumber(groupRows.length)} SKUs · {formatNumber(totals.colorCount)} cores · {formatNumber(totals.sizeCount)} tamanhos
</p>
<h1 className="truncate text-2xl font-bold text-zinc-900 dark:text-dark-text" title={groupName}>{groupName}</h1>
<div className="mt-2 flex flex-wrap items-center gap-2">
<ProductTypeBadge type={totals.productType} />
<span className="text-xs font-semibold text-dark-muted">{productTypeConfig.description}</span>
</div>
</div>
</div>
</div>
@@ -522,7 +534,10 @@ const ProductGroupDetails = () => {
<td className="px-6 py-2.5 font-mono text-[11px] text-zinc-400 dark:text-dark-muted">#{row.id}</td>
<td className="max-w-0 px-6 py-2.5">
<div className="truncate font-semibold text-zinc-900 dark:text-dark-text" title={row.name}>{row.name}</div>
<div className="text-[10px] font-medium text-zinc-400 dark:text-dark-muted">Preço Atual: {formatCurrency(row.lastPrice)}</div>
<div className="mt-1 flex min-w-0 items-center gap-2">
<ProductTypeBadge type={row.productType} />
<span className="truncate text-[10px] font-medium text-zinc-400 dark:text-dark-muted">Preço Atual: {formatCurrency(row.lastPrice)}</span>
</div>
</td>
<td className="px-6 py-2.5">
<ProductColorBadge label={row.color} className="max-w-[8rem]" />