Compare commits

...

2 Commits

Author SHA1 Message Date
Cauê Faleiros
6608957574 Separate accessory replenishment queue
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m25s
2026-07-17 11:02:20 -03:00
Cauê Faleiros
1dcc565299 Standardize operations page layout 2026-07-17 10:10:59 -03:00
2 changed files with 45 additions and 43 deletions

View File

@@ -260,8 +260,7 @@ const PlanningIssues = () => {
})}
</div>
<div className="rounded-2xl border border-dark-border bg-dark-card shadow-sm">
<div className="flex flex-col gap-3 border-b border-dark-border p-4 xl:flex-row xl:items-center xl:justify-between">
<div className="grid grid-cols-1 gap-3 rounded-2xl border border-dark-border bg-dark-card p-4 shadow-sm xl:grid-cols-[minmax(360px,1fr)_auto] xl:items-center">
<div className="flex flex-col gap-3 md:flex-row md:items-center">
<div className="relative w-full md:w-96">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-dark-muted" />
@@ -292,12 +291,12 @@ const PlanningIssues = () => {
<div className="text-sm font-semibold text-dark-muted">
{formatNumber(rows.length)} SKUs pendentes
</div>
</div>
</div>
{isLoading && !products.length ? (
<PlanningIssuesSkeleton />
) : rows.length ? (
<>
<div className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-sm">
<div className="overflow-x-auto">
<table className="w-full min-w-[1180px] table-fixed text-left text-sm">
<colgroup>
@@ -325,8 +324,8 @@ const PlanningIssues = () => {
<tbody className="divide-y divide-dark-border">
{paginatedRows.map(row => (
<tr key={row.id} className="transition-colors hover:bg-dark-input/50">
<td className="px-6 py-3 font-mono text-[11px] text-dark-muted">#{row.id}</td>
<td className="max-w-0 px-6 py-3">
<td className="px-6 py-2.5 font-mono text-[11px] text-dark-muted">#{row.id}</td>
<td className="max-w-0 px-6 py-2.5">
<div className="truncate font-semibold text-dark-text" title={row.name}>{row.name}</div>
<div className="mt-1 flex min-w-0 items-center gap-2 text-[10px] font-semibold text-dark-muted">
<span className="truncate">Cor: {row.color || '-'}</span>
@@ -335,10 +334,10 @@ const PlanningIssues = () => {
{row.hasOverride && <span className="rounded-full border border-brand-primary/25 bg-brand-primary/10 px-2 py-0.5 text-brand-primary">manual</span>}
</div>
</td>
<td className="px-6 py-3">
<td className="px-6 py-2.5">
<ProductTypeBadge type={row.productType} />
</td>
<td className="px-6 py-3">
<td className="px-6 py-2.5">
<div className="flex flex-wrap gap-1.5">
{row.issues.map(issue => (
<span
@@ -351,10 +350,10 @@ const PlanningIssues = () => {
))}
</div>
</td>
<td className="px-6 py-3 text-xs font-bold text-dark-text">{row.familyLabel}</td>
<td className="px-6 py-3 whitespace-nowrap font-bold text-dark-text">{formatNumber(row.quantitySold)} un.</td>
<td className="px-6 py-3 whitespace-nowrap font-bold text-brand-primary">{formatCurrency(row.revenue)}</td>
<td className="px-4 py-3 text-right">
<td className="px-6 py-2.5 text-xs font-bold text-dark-text">{row.familyLabel}</td>
<td className="px-6 py-2.5 whitespace-nowrap font-bold text-dark-text">{formatNumber(row.quantitySold)} un.</td>
<td className="px-6 py-2.5 whitespace-nowrap font-bold text-brand-primary">{formatCurrency(row.revenue)}</td>
<td className="px-4 py-2.5 text-right">
<div className="flex justify-end gap-2">
<button
type="button"
@@ -396,9 +395,9 @@ const PlanningIssues = () => {
setCurrentPage(1);
}}
/>
</>
</div>
) : (
<div className="flex flex-col items-center justify-center px-6 py-16 text-center">
<div className="flex flex-col items-center justify-center rounded-2xl border border-dark-border bg-dark-card px-6 py-16 text-center shadow-sm">
{products.length ? (
<>
<CheckCircle2 className="h-10 w-10 text-emerald-300" />
@@ -414,7 +413,6 @@ const PlanningIssues = () => {
)}
</div>
)}
</div>
{editingProduct && (
<SkuPlanningModal

View File

@@ -36,7 +36,7 @@ import type { CuttingSettings, DateRange, ProductAnalyticsItem, SupplyFabricPlan
type InventoryTab = 'dashboard' | 'balance' | 'receipts' | 'inventory' | 'movements';
type ReceiptView = 'new' | 'pending' | 'history';
const pageClassName = 'mx-auto flex w-full max-w-7xl flex-col gap-6';
const pageClassName = 'flex w-full flex-col gap-6';
const panelClassName = 'rounded-2xl border border-dark-border bg-dark-card shadow-sm';
const buttonClassName = 'inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text transition-colors hover:border-brand-primary cursor-pointer';
const inputClassName = 'h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary';
@@ -114,7 +114,7 @@ const receiptCategories: Array<{ name: string; icon: typeof Package }> = [
{ name: 'Outro material', icon: ClipboardList },
];
type DemandQueue = 'apparel' | 'inputs' | 'review' | 'dead';
type DemandQueue = 'apparel' | 'accessories' | 'inputs' | 'review' | 'dead';
type DemandRow = ProductAnalyticsItem & {
productType: ProductTypeKey;
@@ -130,6 +130,7 @@ type DemandRow = ProductAnalyticsItem & {
const demandQueueLabels: Record<DemandQueue, string> = {
apparel: 'Cortar / Repor',
accessories: 'Repor acessórios',
inputs: 'Comprar insumos',
review: 'Revisar dados',
dead: 'Estoque parado',
@@ -137,12 +138,13 @@ const demandQueueLabels: Record<DemandQueue, string> = {
const demandQueueDescriptions: Record<DemandQueue, string> = {
apparel: 'Produtos acabados com demanda ativa e cobertura abaixo da meta.',
inputs: 'Insumos, embalagens e matérias-primas com necessidade de reposição.',
accessories: 'Acessórios acabados com demanda ativa e cobertura abaixo da meta.',
inputs: 'Embalagens, matérias-primas e insumos de produção com necessidade de reposição.',
review: 'SKUs com informações pendentes para qualificar o planejamento.',
dead: 'Itens com saldo disponível e baixa movimentação no período.',
};
const directPurchaseTypes = new Set<ProductTypeKey>(['packaging', 'dtf_input', 'raw_material', 'machine_part', 'finished_accessory']);
const directPurchaseTypes = new Set<ProductTypeKey>(['packaging', 'dtf_input', 'raw_material', 'machine_part']);
const planningReviewTypes = new Set<ProductTypeKey>(['unknown', 'kit_bundle']);
@@ -155,8 +157,8 @@ const Header = ({ title, subtitle, backTo }: { title: string; subtitle: string;
</RouterLink>
)}
<div>
<h1 className="text-2xl font-bold text-dark-text">{title}</h1>
<p className="mt-1 text-sm font-semibold text-dark-muted">{subtitle}</p>
<h1 className="mb-2 text-2xl font-bold text-zinc-900 dark:text-dark-text">{title}</h1>
<p className="font-medium text-zinc-500 dark:text-dark-muted">{subtitle}</p>
</div>
</div>
);
@@ -247,6 +249,8 @@ const buildDemandRow = (
queue = 'review';
} else if (productType === 'finished_apparel') {
queue = 'apparel';
} else if (productType === 'finished_accessory') {
queue = 'accessories';
} else if (directPurchaseTypes.has(productType)) {
queue = 'inputs';
}
@@ -342,7 +346,7 @@ const DemandPlanningScreen = () => {
return row.dailySales > 0 && (row.suggestedUnits > 0 || (row.daysOfCover !== null && row.daysOfCover <= targetCoverageDays));
}).length;
return acc;
}, { apparel: 0, inputs: 0, review: 0, dead: 0 });
}, { apparel: 0, accessories: 0, inputs: 0, review: 0, dead: 0 });
}, [rows, targetCoverageDays]);
const totalPages = Math.ceil(queueRows.length / itemsPerPage);
@@ -364,7 +368,7 @@ const DemandPlanningScreen = () => {
/>
</div>
<div className="grid grid-cols-1 gap-3 md:grid-cols-4">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-5">
{(Object.keys(demandQueueLabels) as DemandQueue[]).map(item => (
<button
key={item}
@@ -384,13 +388,14 @@ const DemandPlanningScreen = () => {
))}
</div>
<div className={`${panelClassName} p-5`}>
<div className="grid grid-cols-1 gap-3 rounded-2xl border border-dark-border bg-dark-card p-4 shadow-sm xl:grid-cols-[minmax(360px,1fr)_auto] xl:items-center">
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
<div>
<h2 className="text-base font-bold text-dark-text">{demandQueueLabels[queue]}</h2>
<p className="mt-1 text-sm font-semibold text-dark-muted">{demandQueueDescriptions[queue]}</p>
</div>
<div className="flex flex-col gap-2 sm:flex-row">
</div>
<div className="flex flex-col gap-2 sm:flex-row xl:justify-end">
<label className="relative min-w-72">
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-dark-muted" />
<input
@@ -435,10 +440,10 @@ const DemandPlanningScreen = () => {
<Download className="h-4 w-4" />
CSV
</button>
</div>
</div>
</div>
<div className="mt-4 grid grid-cols-1 gap-3 md:grid-cols-3">
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
<div className="rounded-xl border border-dark-border bg-dark-input/35 p-4">
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Itens na fila</p>
<p className="mt-2 text-2xl font-bold text-dark-text">{formatNumber(queueRows.length, 0)}</p>
@@ -455,15 +460,15 @@ const DemandPlanningScreen = () => {
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Cobertura alvo</p>
<p className="mt-2 text-2xl font-bold text-dark-text">{targetCoverageDays} dias</p>
</div>
</div>
</div>
{isLoading ? (
<div className={emptyStateClassName}>
<div className={`${panelClassName} ${emptyStateClassName}`}>
<PackageSearch className="h-8 w-8 text-brand-primary" />
<h3 className="text-base font-bold text-dark-text">Carregando dados de produtos...</h3>
</div>
) : queueRows.length ? (
<div className="mt-4 overflow-hidden rounded-xl border border-dark-border">
<div className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-sm">
<div className="overflow-x-auto">
<table className="w-full min-w-[1080px] table-fixed border-collapse">
<colgroup>
@@ -476,7 +481,7 @@ const DemandPlanningScreen = () => {
<col className="w-[130px]" />
<col className="w-[100px]" />
</colgroup>
<thead className="border-b border-dark-border bg-dark-input/40 text-xs font-bold uppercase tracking-widest text-dark-muted">
<thead className="border-b border-dark-border bg-dark-header text-xs font-bold uppercase tracking-widest text-dark-muted">
<tr>
<th scope="col" className="px-4 py-3 text-left">SKU</th>
<th scope="col" className="px-4 py-3 text-left">Produto</th>
@@ -488,27 +493,27 @@ const DemandPlanningScreen = () => {
<th scope="col" className="px-4 py-3 text-right">Ações</th>
</tr>
</thead>
<tbody className="divide-y divide-dark-border bg-dark-card">
<tbody className="divide-y divide-dark-border">
{paginatedRows.map(row => (
<tr key={row.id}>
<td className="px-4 py-3 font-mono text-[11px] text-dark-muted">#{row.id}</td>
<td className="px-4 py-3">
<tr key={row.id} className="transition-colors hover:bg-dark-input/50">
<td className="px-4 py-2.5 font-mono text-[11px] text-dark-muted">#{row.id}</td>
<td className="px-4 py-2.5">
<p className="truncate text-sm font-bold text-dark-text" title={row.name}>{row.name}</p>
<p className="mt-1 truncate text-xs font-semibold text-dark-muted">
Cor: {row.color || '-'} · Tam.: {row.size || '-'}
{row.missingData.length ? ` · Falta: ${row.missingData.join(', ')}` : ''}
</p>
</td>
<td className="px-4 py-3"><ProductTypeBadge type={row.productType} /></td>
<td className="px-4 py-3 text-right text-sm font-bold text-dark-text">{formatNumber(row.dailySales, 2)}</td>
<td className="px-4 py-3 text-right text-sm font-bold text-dark-text">{formatNumber(row.stock, 0)} un.</td>
<td className="px-4 py-3 text-right text-sm font-bold text-dark-text">{formatDays(row.daysOfCover)}</td>
<td className={`px-4 py-3 text-right text-sm font-bold ${queue === 'dead' ? 'text-amber-300' : row.suggestedUnits > 0 ? 'text-amber-300' : 'text-emerald-300'}`}>
<td className="px-4 py-2.5"><ProductTypeBadge type={row.productType} /></td>
<td className="px-4 py-2.5 text-right text-sm font-bold text-dark-text">{formatNumber(row.dailySales, 2)}</td>
<td className="px-4 py-2.5 text-right text-sm font-bold text-dark-text">{formatNumber(row.stock, 0)} un.</td>
<td className="px-4 py-2.5 text-right text-sm font-bold text-dark-text">{formatDays(row.daysOfCover)}</td>
<td className={`px-4 py-2.5 text-right text-sm font-bold ${queue === 'dead' ? 'text-amber-300' : row.suggestedUnits > 0 ? 'text-amber-300' : 'text-emerald-300'}`}>
{queue === 'dead'
? new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(row.stock * row.lastPrice)
: `${formatNumber(row.suggestedUnits, 0)} un.`}
</td>
<td className="px-4 py-3 text-right">
<td className="px-4 py-2.5 text-right">
<div className="flex justify-end gap-2">
{queue === 'review' && (
<RouterLink
@@ -554,13 +559,12 @@ const DemandPlanningScreen = () => {
/>
</div>
) : (
<div className={emptyStateClassName}>
<div className={`${panelClassName} ${emptyStateClassName}`}>
<PackageSearch className="h-8 w-8 text-brand-primary" />
<h3 className="text-base font-bold text-dark-text">Nada nesta fila</h3>
<p className="text-sm font-semibold text-dark-muted">Ajuste a busca, período ou cobertura alvo.</p>
</div>
)}
</div>
</div>
);
};