import { useEffect, useMemo, useState } from 'react'; import { Link, useOutletContext } from 'react-router-dom'; import { AlertTriangle, CheckCircle2, Eye, PackageSearch, Pencil, Search } from 'lucide-react'; import DateRangePicker from '../components/DateRangePicker'; import PaginationControls from '../components/PaginationControls'; import ProductTypeBadge from '../components/ProductTypeBadge'; import RefreshStatus from '../components/RefreshStatus'; import SkuPlanningModal from '../components/SkuPlanningModal'; import { classifyCutFamily, CUT_FAMILY_RULES, type CutFamilyKey } from '../analytics/cutting'; import { fetchCuttingSettings, fetchProductAnalytics, saveCuttingSettings } from '../dataService'; import { getProductTypeConfig, resolveProductType, type ProductTypeKey } from '../productClassification'; import { parseProductName } from '../productParsing'; import type { CutProductOverride, CuttingSettings, DateRange, ProductAnalyticsItem } from '../types'; type IssueKey = 'review_type' | 'missing_cut_family' | 'missing_color' | 'missing_size' | 'missing_yield'; type IssueFilter = 'all' | IssueKey; type PlanningIssueRow = ProductAnalyticsItem & { productType: ProductTypeKey; color: string; size: string; familyKey: CutFamilyKey | ''; familyLabel: string; issues: IssueKey[]; priorityScore: number; hasOverride: boolean; }; const issueLabels: Record = { review_type: 'Tipo', missing_cut_family: 'Família', missing_color: 'Cor', missing_size: 'Tamanho', missing_yield: 'Rendimento' }; const issueHelp: Record = { review_type: 'O classificador não conseguiu identificar o tipo do SKU.', missing_cut_family: 'SKU de vestuário não caiu em uma família de corte confiável.', missing_color: 'SKU de vestuário não tem cor clara para planejamento.', missing_size: 'SKU de vestuário não tem tamanho claro para planejamento.', missing_yield: 'A família existe, mas ainda falta rendimento em unidades por rolo.' }; const issueFilters: Array<{ value: IssueFilter; label: string }> = [ { value: 'all', label: 'Todos' }, { value: 'review_type', label: issueLabels.review_type }, { value: 'missing_cut_family', label: issueLabels.missing_cut_family }, { value: 'missing_color', label: issueLabels.missing_color }, { value: 'missing_size', label: issueLabels.missing_size }, { value: 'missing_yield', label: issueLabels.missing_yield } ]; const familyLabels = new Map([ ...CUT_FAMILY_RULES.map(rule => [rule.key, `${rule.materialLabel} · ${rule.label}`] as const), ['OUTROS', 'Sem regra'] ]); const formatNumber = (value: number, maximumFractionDigits = 0) => ( new Intl.NumberFormat('pt-BR', { maximumFractionDigits }).format(value) ); const formatCurrency = (value: number) => ( new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value) ); const buildIssues = ( product: ProductAnalyticsItem, settings: CuttingSettings ): PlanningIssueRow => { const override = settings.productOverrides[product.id]; const metadata = parseProductName(product.name); const productType = resolveProductType(product.name, override); const familyKey = override?.familyKey || classifyCutFamily(metadata.baseName).key; const color = override?.color || metadata.color; const size = (override?.size || metadata.size).toUpperCase(); const issues: IssueKey[] = []; if (productType === 'unknown') issues.push('review_type'); if (productType === 'finished_apparel') { if (familyKey === 'OUTROS') issues.push('missing_cut_family'); if (!color) issues.push('missing_color'); if (!size) issues.push('missing_size'); if (familyKey !== 'OUTROS' && !settings.familyYields[familyKey]) issues.push('missing_yield'); } return { ...product, productType, color, size, familyKey, familyLabel: familyLabels.get(familyKey) || '-', issues, priorityScore: (product.quantitySold * 2) + (product.revenue / 100), hasOverride: Boolean(override) }; }; const PlanningIssuesSkeleton = () => (
{[0, 1, 2, 3, 4, 5, 6].map(item =>
)}
{[0, 1, 2, 3, 4, 5].map(row => (
{[0, 1, 2, 3, 4, 5, 6].map(item =>
)}
))}
); const PlanningIssues = () => { const { dateRange, setDateRange } = useOutletContext<{ dateRange: DateRange, setDateRange: (range: DateRange) => void }>(); const [products, setProducts] = useState([]); const [settings, setSettings] = useState({ familyYields: {}, productOverrides: {} }); const [isLoading, setIsLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); const [issueFilter, setIssueFilter] = useState('all'); const [editingProduct, setEditingProduct] = useState(null); const [isSaving, setIsSaving] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(20); useEffect(() => { let isMounted = true; const loadData = async () => { setIsLoading(true); const [productData, planningSettings] = await Promise.all([ fetchProductAnalytics(dateRange), fetchCuttingSettings() ]); if (isMounted) { setProducts(productData); setSettings(planningSettings); setIsLoading(false); } }; void loadData(); return () => { isMounted = false; }; }, [dateRange]); const rows = useMemo(() => { const normalizedSearch = searchTerm.trim().toLowerCase(); return products .map(product => buildIssues(product, settings)) .filter(row => row.issues.length > 0) .filter(row => issueFilter === 'all' || row.issues.includes(issueFilter)) .filter(row => { if (!normalizedSearch) return true; const typeLabel = getProductTypeConfig(row.productType).label.toLowerCase(); return ( row.id.toLowerCase().includes(normalizedSearch) || row.name.toLowerCase().includes(normalizedSearch) || row.color.toLowerCase().includes(normalizedSearch) || row.size.toLowerCase().includes(normalizedSearch) || row.familyLabel.toLowerCase().includes(normalizedSearch) || typeLabel.includes(normalizedSearch) ); }) .sort((a, b) => { if (b.issues.length !== a.issues.length) return b.issues.length - a.issues.length; return b.priorityScore - a.priorityScore; }); }, [issueFilter, products, searchTerm, settings]); const issueCounts = useMemo(() => { const counts = new Map(); products .map(product => buildIssues(product, settings)) .forEach(row => row.issues.forEach(issue => counts.set(issue, (counts.get(issue) || 0) + 1))); return counts; }, [products, settings]); const saveProductOverride = async (productId: string, override: CutProductOverride | null) => { const productOverrides = { ...settings.productOverrides }; if (override) { productOverrides[productId] = override; } else { delete productOverrides[productId]; } const nextSettings = { ...settings, productOverrides }; setIsSaving(true); try { const savedSettings = await saveCuttingSettings(nextSettings); setSettings(savedSettings); setEditingProduct(null); } finally { setIsSaving(false); } }; const totalPages = Math.ceil(rows.length / itemsPerPage); const safeCurrentPage = Math.min(currentPage, totalPages || 1); const startIndex = (safeCurrentPage - 1) * itemsPerPage; const paginatedRows = rows.slice(startIndex, startIndex + itemsPerPage); const isRefreshing = isLoading && products.length > 0; return (

Dados Pendentes

Fila principal para revisar tipo, cor, tamanho, família e rendimento dos SKUs.

Cadastros { setDateRange(range); setCurrentPage(1); }} />
{issueFilters.filter(option => option.value !== 'all').map(option => { const count = issueCounts.get(option.value as IssueKey) || 0; return ( ); })}
{ setSearchTerm(event.target.value); setCurrentPage(1); }} placeholder="Buscar SKU, produto, cor, tipo..." className="h-10 w-full rounded-xl border border-dark-border bg-dark-input pl-10 pr-3 text-sm font-semibold text-dark-text outline-none transition-colors placeholder:text-dark-muted focus:border-brand-primary" />
{formatNumber(rows.length)} SKUs pendentes
{isLoading && !products.length ? ( ) : rows.length ? (
{paginatedRows.map(row => ( ))}
SKU Produto Tipo Pendências Família Vendido Receita Ações
#{row.id}
{row.name}
Cor: {row.color || '-'} · Tam.: {row.size || '-'} {row.hasOverride && manual}
{row.issues.map(issue => ( {issueLabels[issue]} ))}
{row.familyLabel} {formatNumber(row.quantitySold)} un. {formatCurrency(row.revenue)}
{ setItemsPerPage(size); setCurrentPage(1); }} />
) : (
{products.length ? ( <>

Nenhum dado pendente neste filtro.

Ajuste o filtro ou o período para revisar outros SKUs.

) : ( <>

Sem produtos no período.

Altere o intervalo de datas para carregar a fila.

)}
)} {editingProduct && ( setEditingProduct(null)} onSave={(override) => saveProductOverride(editingProduct.id, override)} /> )}
); }; export default PlanningIssues;