import { useEffect, useMemo, useState } from 'react'; import { Loader2, Package, RefreshCw, Ruler, Save, Tags, Trash2 } from 'lucide-react'; import { deleteCatalogCategory, deleteCatalogProduct, deleteConsumptionReference, fetchCatalogSummary, saveCatalogCategory, saveCatalogProduct, saveConsumptionReference } from '../dataService'; import type { CatalogCategory, CatalogProduct, CatalogProductType, CatalogSummary, ConsumptionReference } from '../types'; import { formatColorLabel } from '../displayFormatters'; type RegistrationTab = 'products' | 'categories' | 'references'; type SaveStatus = 'idle' | 'saving' | 'saved' | 'error'; const emptyCatalog: CatalogSummary = { categories: [], products: [], consumptionReferences: [] }; const sizeOptions = ['2', '4', '6', '8', '10', '12', '14', '16', 'PP', 'P', 'M', 'G', 'GG', 'XG', 'G1', 'G2', 'G3', 'G4', 'G5']; const defaultSizeAreas: Record = { P: '0.78', M: '0.85', G: '0.92', GG: '1.00', XG: '1.08', G1: '1.08', G2: '1.16', G3: '1.24', G4: '1.32', G5: '1.40' }; const rawMaterialSubcategories = [ { value: 'fio', label: 'Fio' }, { value: 'malha', label: 'Malha' }, { value: 'ribana', label: 'Ribana' }, { value: 'malha_fria', label: 'Malha Fria' }, { value: 'meia_malha', label: 'Meia Malha 100% Algodao' }, { value: 'moletom', label: 'Moletom' }, { value: 'dry_fit', label: 'Dry Fit' }, { value: 'piquet', label: 'Piquet / Polo' }, { value: 'pima', label: 'Pima' }, { value: 'poliamida', label: 'Poliamida' }, { value: 'outra', label: 'Outra' } ]; const listPanelClassName = 'rounded-2xl border border-dark-border bg-dark-card shadow-sm'; const listHeaderClassName = 'flex min-h-[73px] flex-col gap-3 border-b border-dark-border p-4 md:flex-row md:items-center md:justify-between'; const formPanelClassName = 'rounded-2xl border border-dark-border bg-dark-card p-4 shadow-sm'; const emptyStateClassName = 'flex min-h-[280px] items-center justify-center px-4 py-10 text-center text-sm font-semibold text-dark-muted'; const labelClassName = 'text-xs font-bold text-dark-muted'; const inputClassName = 'mt-1 h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text outline-none focus:border-brand-primary'; const formatNumber = (value: number | null | undefined, maximumFractionDigits = 2) => ( value === null || value === undefined ? '-' : new Intl.NumberFormat('pt-BR', { maximumFractionDigits }).format(value) ); const numericMapFromStrings = (values: Record) => ( Object.entries(values).reduce>((normalized, [key, value]) => { const number = Number(value.replace(',', '.')); if (Number.isFinite(number) && number > 0) normalized[key] = number; return normalized; }, {}) ); const getAverageYield = (reference: ConsumptionReference) => { const yields = Object.values(reference.sizeYields || {}); if (yields.length) return yields.reduce((total, value) => total + value, 0) / yields.length; return reference.generalYield; }; const Registrations = () => { const [catalog, setCatalog] = useState(emptyCatalog); const [isLoading, setIsLoading] = useState(true); const [activeTab, setActiveTab] = useState('products'); const [productFilter, setProductFilter] = useState<'all' | CatalogProductType>('all'); const [status, setStatus] = useState('idle'); const [feedback, setFeedback] = useState(''); const [categoryForm, setCategoryForm] = useState({ name: '', description: '' }); const [productForm, setProductForm] = useState({ type: 'finished_product' as CatalogProductType, sku: '', name: '', categoryId: '', composition: '', notes: '', gramature: '', materialYield: '', widthCm: '', color: '', subcategory: 'malha', sizes: ['P', 'M', 'G', 'GG'] as string[] }); const [referenceForm, setReferenceForm] = useState({ productId: '', materialProductId: '', color: '', generalYield: '', gramature: '', efficiencyPercent: '85', ribGPerPiece: '', materialCostPerKg: '' }); const [sizeAreas, setSizeAreas] = useState>(defaultSizeAreas); const [sizeYields, setSizeYields] = useState>({}); const loadCatalog = async () => { setIsLoading(true); const summary = await fetchCatalogSummary(); setCatalog(summary); setIsLoading(false); }; useEffect(() => { let isMounted = true; fetchCatalogSummary().then(summary => { if (!isMounted) return; setCatalog(summary); setIsLoading(false); }).catch(error => { console.error('Initial catalog load failed', error); if (!isMounted) return; setIsLoading(false); }); return () => { isMounted = false; }; }, []); const finishedProducts = useMemo( () => catalog.products.filter(product => product.type === 'finished_product'), [catalog.products] ); const rawMaterials = useMemo( () => catalog.products.filter(product => product.type === 'raw_material'), [catalog.products] ); const selectedReferenceProduct = useMemo( () => catalog.products.find(product => String(product.id) === referenceForm.productId), [catalog.products, referenceForm.productId] ); const referenceSizes = selectedReferenceProduct?.sizes?.length ? selectedReferenceProduct.sizes : ['P', 'M', 'G', 'GG']; const filteredProducts = useMemo(() => { if (productFilter === 'all') return catalog.products; return catalog.products.filter(product => product.type === productFilter); }, [catalog.products, productFilter]); const runAction = async (action: () => Promise, successMessage: string) => { setStatus('saving'); setFeedback(''); try { await action(); await loadCatalog(); setStatus('saved'); setFeedback(successMessage); } catch (error) { setStatus('error'); setFeedback(error instanceof Error ? error.message : 'Nao foi possivel salvar.'); } }; const saveCategory = () => runAction(async () => { await saveCatalogCategory(categoryForm); setCategoryForm({ name: '', description: '' }); }, 'Categoria salva.'); const saveProduct = () => runAction(async () => { await saveCatalogProduct({ ...productForm, categoryId: productForm.categoryId ? Number(productForm.categoryId) : null, sizes: productForm.type === 'finished_product' ? productForm.sizes : [] }); setProductForm(current => ({ ...current, sku: '', name: '', composition: '', notes: '', gramature: '', materialYield: '', widthCm: '', color: '' })); }, 'Produto salvo.'); const calculateSizeYields = () => { const gramature = Number(referenceForm.gramature.replace(',', '.')); const efficiency = Number(referenceForm.efficiencyPercent.replace(',', '.')) || 85; const rib = Number(referenceForm.ribGPerPiece.replace(',', '.')) || 0; if (!Number.isFinite(gramature) || gramature <= 0) { setStatus('error'); setFeedback('Informe a gramatura para calcular o rendimento.'); return; } const nextYields = referenceSizes.reduce>((values, size) => { const area = Number((sizeAreas[size] || '').replace(',', '.')); if (!Number.isFinite(area) || area <= 0) return values; const fabricGrams = gramature * area / (efficiency / 100); const pieceGrams = fabricGrams + rib; const yieldValue = pieceGrams > 0 ? 1000 / pieceGrams : 0; values[size] = yieldValue.toFixed(2); return values; }, {}); setSizeYields(nextYields); setStatus('idle'); setFeedback(`${Object.keys(nextYields).length} tamanhos calculados.`); }; const saveReference = () => runAction(async () => { await saveConsumptionReference({ ...referenceForm, productId: Number(referenceForm.productId), materialProductId: referenceForm.materialProductId ? Number(referenceForm.materialProductId) : null, sizeAreas: numericMapFromStrings(sizeAreas), sizeYields: numericMapFromStrings(sizeYields) }); setReferenceForm(current => ({ ...current, productId: '', materialProductId: '', color: '', generalYield: '' })); setSizeYields({}); }, 'Referencia salva.'); const removeCategory = (category: CatalogCategory) => runAction( () => deleteCatalogCategory(category.id), `Categoria ${category.name} excluida.` ); const removeProduct = (product: CatalogProduct) => runAction( () => deleteCatalogProduct(product.id), `Produto ${product.sku} excluido.` ); const removeReference = (reference: ConsumptionReference) => runAction( () => deleteConsumptionReference(reference.id), `Referencia ${reference.productSku} excluida.` ); const toggleProductSize = (size: string) => { setProductForm(current => ({ ...current, sizes: current.sizes.includes(size) ? current.sizes.filter(item => item !== size) : [...current.sizes, size] })); }; const tabItems: Array<{ key: RegistrationTab; label: string; icon: typeof Package; count: number }> = [ { key: 'products', label: 'Produtos', icon: Package, count: catalog.products.length }, { key: 'categories', label: 'Categorias', icon: Tags, count: catalog.categories.length }, { key: 'references', label: 'Referencia de Consumo', icon: Ruler, count: catalog.consumptionReferences.length } ]; return (

Cadastros

Base de produtos, categorias e referencias de consumo usadas pelo corte.

{tabItems.map(item => { const Icon = item.icon; const isActive = activeTab === item.key; return ( ); })}
{feedback && (
{feedback}
)} {isLoading ? (
) : ( <> {activeTab === 'products' && (

Produtos cadastrados

Produtos acabados e materias-primas do corte.

{filteredProducts.map(product => (
{product.sku} {product.type === 'finished_product' ? 'Produto acabado' : 'Materia-prima'}

{product.name}

{[product.categoryName, product.composition, product.color ? formatColorLabel(product.color) : ''] .filter(Boolean) .join(' · ') || 'Sem detalhes'}

))} {!filteredProducts.length && (
Nenhum produto cadastrado.
)}

Cadastrar produto / materia-prima

{productForm.type === 'raw_material' ? ( <>
) : (

Tamanhos disponiveis

{sizeOptions.map(size => ( ))}
)}
)} {activeTab === 'categories' && (

Categorias cadastradas

Classificacao usada no cadastro de produtos.

{catalog.categories.map(category => (

{category.name}

{category.description || 'Sem descricao'}

))} {!catalog.categories.length &&
Nenhuma categoria cadastrada.
}

Nova categoria

)} {activeTab === 'references' && (

Referencias cadastradas

Rendimento pç/kg por produto, malha, cor e tamanho.

{catalog.consumptionReferences.length} referencias
{catalog.consumptionReferences.map(reference => (
{reference.productSku} {Object.keys(reference.sizeYields || {}).length ? 'Por tamanho' : 'Geral'}

{reference.productName}

{[reference.materialName, reference.color ? formatColorLabel(reference.color) : 'todas as cores'].filter(Boolean).join(' · ')}

{!!Object.keys(reference.sizeYields || {}).length && (
{Object.entries(reference.sizeYields).map(([size, value]) => ( {size}: {formatNumber(value)} ))}
)}
{formatNumber(getAverageYield(reference), 3)} pç/kg
{Object.keys(reference.sizeYields || {}).length ? 'media' : 'geral'}
))} {!catalog.consumptionReferences.length && (
Nenhuma referencia cadastrada.
)}

Nova referencia de consumo

Defina pç/kg por produto, malha e cor para o calculo do corte.

Calculo por gramatura

Opcional. Preenche o pç/kg dos tamanhos abaixo.

Rendimento por tamanho

area m² / pç/kg
Tam. Area Rendimento
{referenceSizes.map(size => (
{size}
setSizeAreas(current => ({ ...current, [size]: event.target.value }))} placeholder="area" className="h-8 w-full rounded-lg border border-dark-border bg-dark-input px-2 text-xs font-bold text-dark-text outline-none focus:border-brand-primary" /> setSizeYields(current => ({ ...current, [size]: event.target.value }))} placeholder="pç/kg" className="h-8 w-full rounded-lg border border-dark-border bg-dark-input px-2 text-xs font-bold text-dark-text outline-none focus:border-brand-primary" />
))}
)} )}
); }; export default Registrations;