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:
@@ -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]" />
|
||||
|
||||
Reference in New Issue
Block a user