import React, { useEffect, useState } from 'react'; import { Card } from '../components/Card'; import { Button } from '../components/Button'; import { Plus, Trash2, Save, FileText, TrendingUp, ChevronRight, Calculator, Droplet, Weight, Ruler, AlertTriangle, Printer, RefreshCw } from 'lucide-react'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ComposedChart, Area } from 'recharts'; import * as yieldStudyService from '../services/yieldStudyService'; import * as dataSheetService from '../services/dataSheetService'; import * as geometryService from '../services/geometryTypeService'; import type { YieldStudy, TechnicalDataSheet, PieceCategory, GeometryType } from '../types'; import { format } from 'date-fns'; import { useAuth } from '../context/useAuth'; import { useToast } from '../hooks/useToast'; // Estilos globais para impressão const printStyles = ` /* Esconde o container de relatório na tela normal */ .print-report-container { display: none; } @media print { @page { size: A4 portrait; margin: 1cm; } /* Esconde a interface original escondendo todos os filhos diretos de report-root */ .report-root > * { display: none !important; } /* Exceto o container de relatório */ .report-root > .print-report-container { display: block !important; visibility: visible !important; position: absolute !important; left: 0 !important; top: 0 !important; width: 100% !important; height: auto !important; background: white !important; color: black !important; } .print-report-container * { visibility: visible !important; } } `; export const YieldStudyDashboard: React.FC = () => { const { isGuest, isAdmin } = useAuth(); const { showGuestWarning } = useToast(); const [studies, setStudies] = useState([]); const [sheets, setSheets] = useState([]); const [geometryTypes, setGeometryTypes] = useState([]); const [selectedStudy, setSelectedStudy] = useState(null); const [loading, setLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); useEffect(() => { const style = document.createElement('style'); style.innerHTML = printStyles; document.head.appendChild(style); return () => { document.head.removeChild(style); }; }, []); useEffect(() => { fetchInitialData(); }, []); // Auto-recalculate when study is selected or sheets change useEffect(() => { if (selectedStudy && sheets.length > 0) { // Garantir que estamos usando o ID correto se vier populado do backend const ds = selectedStudy.dataSheetId as unknown; const currentId = (typeof ds === 'object' && ds !== null) ? (ds as { _id?: string, id?: string })._id || (ds as { _id?: string, id?: string }).id : ds as string; if (currentId && typeof currentId === 'string') { recalculateStudy({ ...selectedStudy, dataSheetId: currentId as string }); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedStudy?.id, sheets.length > 0]); const fetchInitialData = async () => { try { const [studiesRes, sheetsRes, geoRes] = await Promise.all([ yieldStudyService.getStudies(), dataSheetService.getDataSheets(), geometryService.getAllTypes() ]); // Normalizar estudos para garantir que as categorias tenham o campo 'id' const normalizedStudies = (studiesRes.data || []).map((s: YieldStudy) => ({ ...s, categories: (s.categories || []).map(cat => ({ ...cat, id: cat.id || (cat as any)._id // Garante ID único })) })); setStudies(normalizedStudies); setSheets(sheetsRes.data); setGeometryTypes(geoRes.data); } catch (error) { console.error('Error fetching data', error); } finally { setLoading(false); } }; const handleCreateStudy = async () => { if (isGuest()) { showGuestWarning(); return; } const newStudy: Partial = { name: `Novo Estudo - ${format(new Date(), 'dd/MM/yy HH:mm')}`, dataSheetId: sheets.length > 0 ? sheets[0].id : '', targetDft: 120, dilutionPercent: 10, categories: [ { id: crypto.randomUUID(), name: 'Estrutura Primária', weight: 1000, area: 0, // Nova propriedade para área em m² historicalYield: 150, // L/t (convertido de 0.15 L/kg) historicalDft: 120, // µm efficiency: 85 } ], totalWeight: 1000, estimatedPaintVolume: 0, estimatedReducerVolume: 0, averageComplexity: 1 }; try { const res = await yieldStudyService.createStudy(newStudy); const normalized = { ...res.data, categories: res.data.categories.map((cat: any) => ({ ...cat, id: cat.id || cat._id })) }; setStudies([normalized, ...studies]); setSelectedStudy(normalized); } catch (error) { console.error('Error creating study', error); } }; const handleSaveStudy = async () => { if (isGuest()) { showGuestWarning(); return; } if (!selectedStudy) return; setIsSaving(true); try { // Recalcular para garantir que os totais estejam sincronizados antes de salvar const res = await yieldStudyService.updateStudy(selectedStudy.id, selectedStudy); setStudies(studies.map(s => s.id === selectedStudy.id ? res.data : s)); alert('Estudo salvo com sucesso!'); } catch (error) { console.error('Error saving study', error); alert('Erro ao salvar estudo.'); } finally { setIsSaving(false); } }; const handleDeleteStudy = async (id: string, e: React.MouseEvent) => { e.stopPropagation(); if (isGuest()) { showGuestWarning(); return; } if (!confirm('Excluir este estudo?')) return; try { await yieldStudyService.deleteStudy(id); setStudies(studies.filter(s => s.id !== id)); if (selectedStudy?.id === id) setSelectedStudy(null); } catch (error) { console.error('Error deleting study', error); alert('Erro ao deletar estudo.'); } }; const handlePrint = () => { window.print(); }; // Função de atualização atômica - Recalcula tudo imediatamente const updateCategory = (id: string, updates: Partial) => { if (!selectedStudy) return; // Se o ID for undefined, algo está muito errado com a normalização if (!id) { console.error('updateCategory called with no id'); return; } // 1. Aplica a atualização na categoria específica const newCategories = selectedStudy.categories.map(c => { const currentId = c.id || (c as any)._id; return currentId === id ? { ...c, ...updates } : c; }); // 2. Delega o recálculo para a função centralizada recalculateStudy({ ...selectedStudy, categories: newCategories }); }; const addCategory = () => { if (!selectedStudy) return; const newCat: PieceCategory = { id: crypto.randomUUID(), name: `Nova Categoria ${selectedStudy.categories.length + 1}`, weight: 0, historicalYield: 0.1, historicalDft: 100, efficiency: 80 }; recalculateStudy({ ...selectedStudy, categories: [...selectedStudy.categories, newCat] }); }; const removeCategory = (id: string) => { if (!selectedStudy) return; const newCategories = selectedStudy.categories.filter(c => c.id !== id); recalculateStudy({ ...selectedStudy, categories: newCategories }); }; // Helper: Encontra a ficha técnica por id ou _id, tratando também objetos populados const findSheet = (dataSheetId: unknown): TechnicalDataSheet | undefined => { if (!dataSheetId) return undefined; // Se for um objeto, extrai o ID const ds = dataSheetId as { id?: string, _id?: string }; const idToFind = typeof dataSheetId === 'object' && dataSheetId !== null ? ds.id || ds._id : dataSheetId as string; return sheets.find(s => s.id === idToFind || (s as unknown as { _id: string })._id === idToFind ); }; const recalculateStudy = (study: YieldStudy) => { // Busca a ficha técnica usando helper const selectedSheet = findSheet(study.dataSheetId); // Normaliza SV: se <= 1 (ex: 0.60), converte para % (60). Default 60. let sv = selectedSheet?.solidsVolume || 60; if (sv <= 1) sv = sv * 100; // ============================================ // CÁLCULO DE EPU (Película Úmida) // Fórmula: EPS = EPU × [SV × (100 - Diluição)] / 10000 // Invertendo: EPU = EPS × 10000 / [SV × (100 - Diluição)] // ============================================ const dilutionFactor = 100 - study.dilutionPercent; const svFactor = sv * dilutionFactor; const calculatedEpu = svFactor > 0 ? Number((study.targetDft * 10000 / svFactor).toFixed(1)) : 0; let totalWeight = 0; let totalVolumeByWeight = 0; let totalVolumeByArea = 0; const updatedCategories = study.categories.map(cat => { const weightInTons = Number(cat.weight) || 0; const yieldPerTon = Number(cat.historicalYield) || 0; const rawVal = Number(cat.efficiency); const val = !isNaN(rawVal) ? rawVal : 20; const isLegacyEfficiency = val > 60; const efficiencyDecimal = isLegacyEfficiency ? val / 100 : (100 - val) / 100; const lossFactor = efficiencyDecimal > 0 ? 1 / efficiencyDecimal : 1; // Cálculo por PESO (L/t) - Não usa mais o fator de eficiência/perda const litrosPeso = weightInTons * yieldPerTon; totalVolumeByWeight += litrosPeso; // Cálculo por ÁREA (m²) - Usa o fator de eficiência/perda let litrosArea = 0; if (cat.area && Number(cat.area) > 0) { const theoreticalVolumeByArea = (Number(cat.area) * study.targetDft) / (sv * 10); litrosArea = theoreticalVolumeByArea * lossFactor; totalVolumeByArea += litrosArea; } totalWeight += weightInTons; return { ...cat, litrosPeso: Number(litrosPeso.toFixed(2)), litrosArea: litrosArea > 0 ? Number(litrosArea.toFixed(2)) : undefined }; }); const reducerVolByWeight = totalVolumeByWeight * (study.dilutionPercent / 100); const reducerVolByArea = totalVolumeByArea * (study.dilutionPercent / 100); setSelectedStudy({ ...study, categories: updatedCategories, totalWeight, estimatedPaintVolume: Number(totalVolumeByWeight.toFixed(2)), estimatedReducerVolume: Number(reducerVolByWeight.toFixed(2)), estimatedPaintVolumeByArea: Number(totalVolumeByArea.toFixed(2)), estimatedReducerVolumeByArea: Number(reducerVolByArea.toFixed(2)), calculatedEpu: calculatedEpu } as YieldStudy & { calculatedEpu: number }); }; const chartData = selectedStudy?.categories.map((c, idx) => { // Prioriza litrosArea para o gráfico se estiver disponível const valorFinal = c.litrosArea && c.litrosArea > 0 ? c.litrosArea : (c.litrosPeso || 0); return { name: c.name, litros: valorFinal, id: c.id || `cat-${idx}` }; }) || []; // Data for deviation projection // Data for deviation projection - Lógica Direta (Mais DFT = Mais Tinta) const projectionData = selectedStudy ? [ { dft: (selectedStudy.targetDft * 0.8).toFixed(0), vol: Number((selectedStudy.estimatedPaintVolume * 0.8).toFixed(1)), label: `-20%` }, { dft: (selectedStudy.targetDft * 0.9).toFixed(0), vol: Number((selectedStudy.estimatedPaintVolume * 0.9).toFixed(1)), label: `-10%` }, { dft: selectedStudy.targetDft.toFixed(0), vol: selectedStudy.estimatedPaintVolume, label: 'ALVO' }, { dft: (selectedStudy.targetDft * 1.1).toFixed(0), vol: Number((selectedStudy.estimatedPaintVolume * 1.1).toFixed(1)), label: '+10%' }, { dft: (selectedStudy.targetDft * 1.3).toFixed(0), vol: Number((selectedStudy.estimatedPaintVolume * 1.3).toFixed(1)), label: '+30%' }, ] : []; if (loading) return
Carregando estudos...
; return (
{/* Page Header */}

Estudo de Rendimento

Análise de consumo e projeção por categoria

{selectedStudy && ( <> )} {isAdmin() && ( )}
{!selectedStudy ? (
{studies.length === 0 ? (

Crie seu primeiro estudo

Compare categorias de peças e projete consumo de tinta para futuras obras.

{isAdmin() && }
) : ( studies.map(study => (
{ const normalized = { ...study, categories: study.categories.map(cat => ({ ...cat, id: cat.id || (cat as any)._id })) }; setSelectedStudy(normalized); }} className="p-8 flex flex-col h-full relative cursor-pointer bg-surface border border-border/40 rounded-[32px] group-hover:border-primary/40 transition-all duration-500 shadow-soft group-hover:shadow-2xl" >
{isAdmin() && ( )}

{study.name}

Volume Estimado {study.estimatedPaintVolume} LITROS
Carga Total {study.totalWeight.toFixed(1)} t
Target DFT {study.targetDft} μm
{format(new Date(study.updatedAt), 'dd MMM yyyy')}
VER DETALHES
)) )}
) : (
{/* Sidebar: Configuracao do Estudo */}

Parâmetros

setSelectedStudy({ ...selectedStudy, name: e.target.value })} placeholder="Nome do Estudo" />
recalculateStudy({ ...selectedStudy, targetDft: Number(e.target.value) })} />
recalculateStudy({ ...selectedStudy, dilutionPercent: Number(e.target.value) })} />
{/* EPU e SV Calculados */}
EPU Calculado
{(selectedStudy as YieldStudy & { calculatedEpu?: number }).calculatedEpu || (() => { const sheet = findSheet(selectedStudy.dataSheetId); let sv = sheet?.solidsVolume || 60; if (sv <= 1) sv *= 100; const dilFactor = 100 - selectedStudy.dilutionPercent; const svFactor = sv * dilFactor; return svFactor > 0 ? (selectedStudy.targetDft * 10000 / svFactor).toFixed(1) : '0'; })() } µm
Película Úmida
{(() => { const sheet = findSheet(selectedStudy.dataSheetId); const hasRealSV = sheet?.solidsVolume && sheet.solidsVolume > 0; let sv = sheet?.solidsVolume || 60; if (sv <= 1) sv *= 100; return ( <> SV da Tinta {hasRealSV ? '✓' : '⚠️'}
{sv.toFixed(0)} %
{hasRealSV ? 'Sólidos por Volume' : 'Valor padrão (edite a ficha)'} ); })()}
{/* Cálculo por Peso */}
Cálculo por Peso (Kg)
Tinta Total
{Math.round(selectedStudy.estimatedPaintVolume)} L
Diluente
{Math.round(selectedStudy.estimatedReducerVolume)} L
Taxa Média {selectedStudy.totalWeight > 0 ? ((selectedStudy.estimatedPaintVolume / selectedStudy.totalWeight).toFixed(2)) : '0.00'} L/t
Peso Total {selectedStudy.totalWeight.toFixed(2)} TON
{/* Cálculo por Área (m²) - Condicional */} {(selectedStudy.estimatedPaintVolumeByArea || 0) > 0 && (
Cálculo por Área (m²)
Tinta Total
{Math.round(selectedStudy.estimatedPaintVolumeByArea || 0)} L
Diluente
{Math.round(selectedStudy.estimatedReducerVolumeByArea || 0)} L
)}
{isAdmin() && ( )}
{/* Main Content: Categories and Details */}
{/* Categorias */}

Setores / Peças

{isAdmin() && ( )}
{selectedStudy.categories.map((cat) => (
{isAdmin() && ( )}
updateCategory(cat.id, { weight: Number(e.target.value) })} title="Peso Bruto em Toneladas" placeholder="0.0" />
updateCategory(cat.id, { historicalYield: Number(e.target.value) })} title="Taxa Histórica L/t (litros por tonelada)" placeholder="0" />
updateCategory(cat.id, { area: Number(e.target.value) })} title="Área em m²" placeholder="0" />
updateCategory(cat.id, { efficiency: Number(e.target.value) })} title="Percentual de Eficiência/Perda (Aplica-se apenas ao cálculo por Área)" placeholder="20" />
Consumo (Peso)
{cat.litrosPeso || '0.0'} L
{cat.litrosArea && cat.litrosArea > 0 && (
Consumo Est. (m²)
{cat.litrosArea} L
)}
))}
{/* Visualizacaos */}

Distribuição por Categoria (L)

Projeção de Desvios

[`${value} L`, 'Consumo']} />

Note que espessuras elevadas reduzem drasticamente o rendimento real, podendo exigir até 30% mais tinta do que o previsto.

)} {/* Template de Impressão (PDF/Relatório) - Visível apenas na impressão */} {selectedStudy && (
{/* Cabeçalho do Relatório */}
TS

Estudo de Rendimento Analítico

Previsão de consumo de tinta e projeção de produtividade

Data: {format(new Date(), 'dd / MM / yyyy')}
Responsável:
{selectedStudy.name.toUpperCase()}
{/* Cards de Resumo (Estilo do Relatório de Obras) */}
Peso Total (Ton)
{selectedStudy.totalWeight.toFixed(2)}

Soma das categorias

Tinta Total (Peso)
{Math.round(selectedStudy.estimatedPaintVolume)} L

Baseado em L/t

Tinta Total (Área)
{Math.round(selectedStudy.estimatedPaintVolumeByArea || 0)} L

Baseado em m²

Taxa Média
{selectedStudy.totalWeight > 0 ? (selectedStudy.estimatedPaintVolume / selectedStudy.totalWeight).toFixed(2) : '0.00'} L/t

Rendimento Global

{/* Tabela de Detalhes */}

Detalhamento por Categoria

Visão analítica por geometria, peso e área

{selectedStudy.categories.map((cat, idx) => ( ))}
Categoria / Tipo Peso (T) Área (m²) Taxa (L/t) Efic. (%) Consumo (L)
{cat.name}
{cat.weight.toFixed(2)} {cat.area ? Math.round(cat.area) : '--'} {cat.historicalYield} {cat.efficiency}%
{cat.litrosPeso} L (p)
{cat.litrosArea &&
{cat.litrosArea} L (a)
}
{/* Parâmetros e Projeção */}

Especificações Técnicas

Tinta Selecionada {findSheet(selectedStudy.dataSheetId)?.name || 'N/A'}
Espessura Alvo (DFT) {selectedStudy.targetDft} µm
Sólidos por Volume (SV) {(() => { let sv = findSheet(selectedStudy.dataSheetId)?.solidsVolume || 60; if (sv <= 1) sv *= 100; return sv.toFixed(0); })()}%
Diluição Aplicada {selectedStudy.dilutionPercent}%
Película Úmida (WFT) {(() => { const sheet = findSheet(selectedStudy.dataSheetId); let sv = sheet?.solidsVolume || 60; if (sv <= 1) sv *= 100; const dilFactor = 100 - selectedStudy.dilutionPercent; const svFactor = sv * dilFactor; return svFactor > 0 ? (selectedStudy.targetDft * 10000 / svFactor).toFixed(1) : '0'; })()} µm

Observações de Estudo

Este estudo é uma estimativa analítica baseada em dados históricos e parâmetros técnicos fornecidos pelo fabricante. O consumo real pode sofrer variações conforme o método de aplicação, condições climáticas e rugosidade do substrato. Espessuras acima do alvo (Target DFT) implicam em consumo exponencialmente maior.

Variações de 10-15% no consumo real são consideradas normais dentro dos padrões industriais de pintura.

{/* Rodapé do Relatório */}
Gerado em: {format(new Date(), 'dd/MM/yyyy')} às {format(new Date(), 'HH:mm')}h
ID do Estudo: {selectedStudy.id}
Assinatura do Responsável
SteelPaint - Sistema de Gestão de Pintura Industrial (GPI)
)}
); };