Compare commits
2 Commits
0cae89277f
...
6608957574
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6608957574 | ||
|
|
1dcc565299 |
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user