Polish product group details layout
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 39s

This commit is contained in:
Cauê Faleiros
2026-07-07 10:43:42 -03:00
parent 566b032fb3
commit ae7ac964f7

View File

@@ -3,9 +3,10 @@ import { Link, useOutletContext, useParams } from 'react-router-dom';
import { DollarSign, Package, Palette, Ruler, TrendingDown, TrendingUp, Warehouse } from 'lucide-react';
import BackButton from '../components/BackButton';
import DateRangePicker from '../components/DateRangePicker';
import PaginationControls from '../components/PaginationControls';
import RefreshStatus from '../components/RefreshStatus';
import { fetchProductAnalytics } from '../dataService';
import { decodeProductGroupKey, normalizeProductText, parseProductName, sortProductSizes } from '../productParsing';
import { decodeProductGroupKey, normalizeProductText, parseProductName } from '../productParsing';
import type { DateRange, ProductAnalyticsItem } from '../types';
type VariantRow = ProductAnalyticsItem & {
@@ -23,6 +24,8 @@ type BreakdownRow = {
skuCount: number;
};
const BREAKDOWN_LIMIT = 12;
const COLOR_SWATCHES: Array<{ pattern: string; color: string }> = [
{ pattern: 'preto', color: '#171717' },
{ pattern: 'branco', color: '#f8fafc' },
@@ -69,6 +72,21 @@ const getSwatchColor = (label: string) => {
return COLOR_SWATCHES.find(item => normalizedLabel.includes(item.pattern.normalize('NFD').replace(/\p{Diacritic}/gu, '')))?.color || '#64748b';
};
const getBarColor = (label: string) => {
const color = getSwatchColor(label);
return `color-mix(in srgb, ${color} 74%, var(--color-dark-text) 26%)`;
};
const ColorSwatch = ({ label, className = 'h-2.5 w-2.5' }: { label: string; className?: string }) => (
<span
className={`${className} shrink-0 rounded-sm`}
style={{
backgroundColor: getSwatchColor(label),
boxShadow: '0 0 0 1px var(--color-dark-card), 0 0 0 2px var(--color-dark-border)'
}}
/>
);
const buildBreakdown = (rows: VariantRow[], field: 'color' | 'size') => {
const totals = new Map<string, BreakdownRow>();
@@ -84,16 +102,7 @@ const buildBreakdown = (rows: VariantRow[], field: 'color' | 'size') => {
});
});
const values = [...totals.values()];
if (field === 'size') {
return values.sort((a, b) => {
const [sortedA, sortedB] = sortProductSizes([a.label, b.label]);
if (sortedA === a.label && sortedB === b.label) return 0;
return sortedA === a.label ? -1 : 1;
});
}
return values.sort((a, b) => b.quantitySold - a.quantitySold);
return [...totals.values()].sort((a, b) => b.quantitySold - a.quantitySold);
};
const BreakdownPanel = ({
@@ -108,6 +117,8 @@ const BreakdownPanel = ({
type: 'color' | 'size';
}) => {
const maxSold = Math.max(...rows.map(row => row.quantitySold), 0);
const visibleRows = rows.slice(0, BREAKDOWN_LIMIT);
const hiddenCount = Math.max(0, rows.length - visibleRows.length);
return (
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
@@ -129,25 +140,22 @@ const BreakdownPanel = ({
</div>
) : (
<div className="space-y-3">
{rows.map(row => {
{visibleRows.map(row => {
const width = maxSold ? Math.max(4, (row.quantitySold / maxSold) * 100) : 0;
const swatchColor = type === 'color' ? getSwatchColor(row.label) : '#25c2ff';
const barColor = type === 'color' ? getBarColor(row.label) : '#25c2ff';
return (
<div key={row.label} className="grid grid-cols-[120px_1fr_92px] items-center gap-3">
<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">
<span
className="h-2.5 w-2.5 shrink-0 rounded-sm border border-white/20"
style={{ backgroundColor: swatchColor }}
/>
{type === 'color' ? <ColorSwatch 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>
</div>
<div className="h-2.5 overflow-hidden rounded-full bg-dark-border">
<div className="h-3 overflow-hidden rounded-full border border-dark-border bg-dark-input">
<div
className="h-full rounded-full"
style={{ width: `${width}%`, backgroundColor: swatchColor, opacity: 0.86 }}
style={{ width: `${width}%`, backgroundColor: barColor }}
/>
</div>
<div className="text-right text-xs">
@@ -157,6 +165,11 @@ const BreakdownPanel = ({
</div>
);
})}
{hiddenCount > 0 && (
<div className="border-t border-dark-border pt-3 text-xs font-semibold text-dark-muted">
+{formatNumber(hiddenCount)} itens fora do top {BREAKDOWN_LIMIT}
</div>
)}
</div>
)}
</div>
@@ -195,6 +208,8 @@ const ProductGroupDetails = () => {
}>();
const [products, setProducts] = useState<ProductAnalyticsItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(20);
const groupName = useMemo(() => {
if (!groupKey) return '';
@@ -271,6 +286,10 @@ const ProductGroupDetails = () => {
const colorBreakdown = useMemo(() => buildBreakdown(groupRows, 'color'), [groupRows]);
const sizeBreakdown = useMemo(() => buildBreakdown(groupRows, 'size'), [groupRows]);
const isRefreshing = isLoading && products.length > 0;
const totalPages = Math.ceil(groupRows.length / itemsPerPage);
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
const startIndex = (safeCurrentPage - 1) * itemsPerPage;
const paginatedRows = groupRows.slice(startIndex, startIndex + itemsPerPage);
if (isLoading && products.length === 0) {
return <ProductGroupDetailsSkeleton />;
@@ -306,7 +325,13 @@ const ProductGroupDetails = () => {
</div>
</div>
<DateRangePicker dateRange={dateRange} onChange={setDateRange} />
<DateRangePicker
dateRange={dateRange}
onChange={(range) => {
setDateRange(range);
setCurrentPage(1);
}}
/>
</div>
<RefreshStatus isRefreshing={isRefreshing} />
@@ -370,9 +395,14 @@ const ProductGroupDetails = () => {
</div>
<div className="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-dark-border dark:bg-dark-card">
<div className="border-b border-zinc-100 px-6 py-4 dark:border-dark-border">
<h3 className="text-base font-bold text-zinc-900 dark:text-dark-text">Variações do grupo</h3>
<p className="mt-1 text-sm font-medium text-zinc-500 dark:text-dark-muted">SKUs, cores e tamanhos que formam este grupo.</p>
<div className="flex flex-col gap-2 border-b border-zinc-100 px-6 py-4 dark:border-dark-border md:flex-row md:items-end md:justify-between">
<div>
<h3 className="text-base font-bold text-zinc-900 dark:text-dark-text">Variações do grupo</h3>
<p className="mt-1 text-sm font-medium text-zinc-500 dark:text-dark-muted">SKUs, cores e tamanhos que formam este grupo.</p>
</div>
<span className="text-xs font-bold uppercase tracking-widest text-zinc-400 dark:text-dark-muted">
{formatNumber(groupRows.length)} SKUs
</span>
</div>
<div className="overflow-x-auto">
<table className="w-full min-w-[1100px] table-fixed text-left text-sm">
@@ -401,7 +431,7 @@ const ProductGroupDetails = () => {
</tr>
</thead>
<tbody className="divide-y divide-zinc-100 dark:divide-dark-border">
{groupRows.map(row => (
{paginatedRows.map(row => (
<tr key={row.id} className="transition-colors hover:bg-zinc-50/80 dark:hover:bg-dark-input/50">
<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">
@@ -410,10 +440,7 @@ const ProductGroupDetails = () => {
</td>
<td className="px-6 py-2.5">
<span className="inline-flex max-w-full items-center gap-2 rounded-full border border-sky-400/25 bg-sky-400/10 px-2.5 py-1 text-xs font-bold text-sky-300">
<span
className="h-2 w-2 shrink-0 rounded-sm border border-white/20"
style={{ backgroundColor: getSwatchColor(row.color || 'Sem cor') }}
/>
<ColorSwatch label={row.color || 'Sem cor'} className="h-2 w-2" />
<span className="truncate">{row.color || '-'}</span>
</span>
</td>
@@ -445,6 +472,22 @@ const ProductGroupDetails = () => {
</tbody>
</table>
</div>
<PaginationControls
totalItems={groupRows.length}
currentPage={safeCurrentPage}
totalPages={totalPages}
pageSize={itemsPerPage}
pageSizeOptions={[10, 20, 50, 100]}
itemLabel="SKUs"
pageSizeLabel="SKUs por página"
startIndex={startIndex}
endIndex={Math.min(startIndex + itemsPerPage, groupRows.length)}
onPageChange={setCurrentPage}
onPageSizeChange={(pageSize) => {
setItemsPerPage(pageSize);
setCurrentPage(1);
}}
/>
</div>
</div>
</div>