Files
GPI/src/client/pages/YieldStudyDashboard.tsx

1067 lines
69 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<YieldStudy[]>([]);
const [sheets, setSheets] = useState<TechnicalDataSheet[]>([]);
const [geometryTypes, setGeometryTypes] = useState<GeometryType[]>([]);
const [selectedStudy, setSelectedStudy] = useState<YieldStudy | null>(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<YieldStudy> = {
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<PieceCategory>) => {
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 <div className="p-8 text-center text-text-muted">Carregando estudos...</div>;
return (
<div className="space-y-10 animate-in fade-in duration-700 pb-12 report-root">
{/* Page Header */}
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6 pb-2">
<div className="space-y-2">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-2xl bg-primary/10 flex items-center justify-center text-primary shadow-lg shadow-primary/5">
<TrendingUp className="w-8 h-8" />
</div>
<div>
<h1 className="text-3xl md:text-5xl font-black text-text-main tracking-tight mb-0">Estudo de Rendimento</h1>
<p className="text-sm text-text-muted font-medium tracking-widest uppercase">Análise de consumo e projeção por categoria</p>
</div>
</div>
</div>
<div className="flex gap-4 no-print">
{selectedStudy && (
<>
<Button
variant="secondary"
className="border-border/50 h-14"
onClick={() => {
// Force refresh: reload sheets and recalculate
fetchInitialData().then(() => {
if (selectedStudy) {
recalculateStudy(selectedStudy);
}
});
}}
title="Atualizar dados da ficha técnica"
>
<RefreshCw className="w-5 h-5" />
</Button>
<Button variant="secondary" className="border-border/50 h-14" onClick={() => setSelectedStudy(null)}>
Voltar à Lista
</Button>
</>
)}
{isAdmin() && (
<Button onClick={handleCreateStudy} size="lg" className="shadow-primary/30 h-14">
<Plus className="w-5 h-5 mr-2" />
Novo Estudo
</Button>
)}
</div>
</div>
{!selectedStudy ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{studies.length === 0 ? (
<div className="col-span-full py-40 text-center bg-surface/50 border border-dashed border-border/40 rounded-[40px]">
<div className="mx-auto h-24 w-24 bg-surface-soft rounded-full flex items-center justify-center text-text-muted/10 mb-8 border border-border/20">
<Calculator className="w-12 h-12" />
</div>
<h3 className="text-2xl font-black text-text-main tracking-tight">Crie seu primeiro estudo</h3>
<p className="mt-2 text-text-muted font-medium max-w-sm mx-auto">Compare categorias de peças e projete consumo de tinta para futuras obras.</p>
{isAdmin() && <Button className="mt-8 border-border/50" variant="secondary" onClick={handleCreateStudy}>Criar Agora</Button>}
</div>
) : (
studies.map(study => (
<div key={study.id} className="group relative">
<div className="absolute -inset-1 bg-gradient-to-br from-primary/20 via-transparent to-blue-500/10 rounded-[32px] blur-xl opacity-0 group-hover:opacity-100 transition-all duration-700"></div>
<Card
onClick={() => {
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"
>
<div className="flex justify-between items-start mb-8">
<div className="p-4 bg-primary/10 rounded-2xl text-primary group-hover:bg-primary group-hover:text-white transition-all duration-500 shadow-sm">
<TrendingUp size={24} />
</div>
{isAdmin() && (
<button
onClick={(e) => handleDeleteStudy(study.id, e)}
className="p-2.5 text-text-muted hover:text-error transition-all rounded-xl hover:bg-error/5"
title="Excluir estudo"
>
<Trash2 size={18} />
</button>
)}
</div>
<h3 className="text-xl font-black text-text-main tracking-tight group-hover:text-primary transition-colors mb-6">{study.name}</h3>
<div className="space-y-4 flex-1">
<div className="p-4 bg-surface-soft/50 rounded-2xl border border-border/20 flex flex-col group/stat hover:border-primary/20 transition-all">
<span className="text-[9px] font-black text-text-muted uppercase tracking-[0.2em] mb-1 group-hover/stat:text-primary/70 transition-colors">Volume Estimado</span>
<span className="text-2xl font-black text-text-main">
{study.estimatedPaintVolume} <span className="text-sm text-text-muted font-bold">LITROS</span>
</span>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col">
<span className="text-[9px] font-black text-text-muted uppercase tracking-[0.2em] mb-1">Carga Total</span>
<span className="text-sm font-black text-text-main">{study.totalWeight.toFixed(1)} t</span>
</div>
<div className="flex flex-col">
<span className="text-[9px] font-black text-text-muted uppercase tracking-[0.2em] mb-1">Target DFT</span>
<span className="text-sm font-black text-text-main">{study.targetDft} <span className="text-[10px] text-text-muted">μm</span></span>
</div>
</div>
</div>
<div className="mt-8 pt-6 border-t border-border/40 flex justify-between items-center text-[10px] text-text-muted font-black tracking-widest uppercase">
<span>{format(new Date(study.updatedAt), 'dd MMM yyyy')}</span>
<div className="flex items-center gap-2 text-primary group-hover:translate-x-1 transition-transform">
VER DETALHES <ChevronRight size={14} />
</div>
</div>
</Card>
</div>
))
)}
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 animate-in fade-in slide-in-from-bottom-6 print-grid">
{/* Sidebar: Configuracao do Estudo */}
<div className="lg:col-span-4 space-y-6 sidebar-container">
<Card className="p-8 space-y-8 bg-surface border border-border/40 rounded-[32px] shadow-soft">
<div className="flex items-center gap-3">
<div className="p-3 bg-primary/10 rounded-xl text-primary">
<FileText size={20} />
</div>
<h3 className="font-black text-xl text-text-main tracking-tight">Parâmetros</h3>
</div>
<div className="space-y-6">
<div className="space-y-2">
<label htmlFor="study-name" className="block text-[10px] font-black text-text-muted uppercase tracking-[0.2em] ml-1">Nome do Estudo</label>
<input
id="study-name"
className="w-full h-12 bg-surface-soft border border-border/40 rounded-xl px-4 text-sm font-bold focus:ring-4 focus:ring-primary/10 transition-all outline-none"
value={selectedStudy.name}
onChange={e => setSelectedStudy({ ...selectedStudy, name: e.target.value })}
placeholder="Nome do Estudo"
/>
</div>
<div className="space-y-2">
<label htmlFor="data-sheet-select" className="block text-[10px] font-black text-text-muted uppercase tracking-[0.2em] ml-1">Tinta Utilizada</label>
<select
id="data-sheet-select"
className="w-full h-12 bg-surface-soft border border-border/40 rounded-xl px-4 text-sm font-bold focus:ring-4 focus:ring-primary/10 transition-all outline-none appearance-none"
value={selectedStudy.dataSheetId}
onChange={e => recalculateStudy({ ...selectedStudy, dataSheetId: e.target.value })}
title="Selecionar Tinta"
>
{sheets.map(s => (
<option key={s.id} value={s.id}>{s.name} ({s.manufacturer})</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label htmlFor="target-dft" className="block text-[10px] font-black text-text-muted uppercase tracking-[0.2em] ml-1">DFT Alvo <span className="normal-case">(µm)</span></label>
<input
id="target-dft"
type="number"
className="w-full h-12 bg-surface-soft border border-border/40 rounded-xl px-4 text-sm font-black text-blue-600 focus:ring-4 focus:ring-primary/10 transition-all outline-none"
value={selectedStudy.targetDft}
onChange={e => recalculateStudy({ ...selectedStudy, targetDft: Number(e.target.value) })}
/>
</div>
<div className="space-y-2">
<label htmlFor="dilution" className="block text-[10px] font-black text-text-muted uppercase tracking-[0.2em] ml-1">Diluição (%)</label>
<input
id="dilution"
type="number"
className="w-full h-12 bg-surface-soft border border-border/40 rounded-xl px-4 text-sm font-black focus:ring-4 focus:ring-primary/10 transition-all outline-none"
value={selectedStudy.dilutionPercent}
onChange={e => recalculateStudy({ ...selectedStudy, dilutionPercent: Number(e.target.value) })}
/>
</div>
</div>
{/* EPU e SV Calculados */}
<div className="grid grid-cols-2 gap-4 mt-4 p-4 bg-amber-500/5 rounded-2xl border border-amber-500/20">
<div className="space-y-1">
<span className="text-[9px] font-black text-amber-600/80 uppercase tracking-wider">EPU Calculado</span>
<div className="text-2xl font-black text-amber-600 leading-none">
{(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';
})()
} <span className="text-xs">µm</span>
</div>
<span className="text-[8px] text-text-muted">Película Úmida</span>
</div>
<div className="space-y-1">
{(() => {
const sheet = findSheet(selectedStudy.dataSheetId);
const hasRealSV = sheet?.solidsVolume && sheet.solidsVolume > 0;
let sv = sheet?.solidsVolume || 60;
if (sv <= 1) sv *= 100;
return (
<>
<span className={`text-[9px] font-black uppercase tracking-wider ${hasRealSV ? 'text-success' : 'text-amber-500'}`}>
SV da Tinta {hasRealSV ? '✓' : '⚠️'}
</span>
<div className={`text-2xl font-black leading-none ${hasRealSV ? 'text-text-main' : 'text-amber-500'}`}>
{sv.toFixed(0)} <span className="text-xs">%</span>
</div>
<span className="text-[8px] text-text-muted">
{hasRealSV ? 'Sólidos por Volume' : 'Valor padrão (edite a ficha)'}
</span>
</>
);
})()}
</div>
</div>
</div>
<div className="pt-8 border-t border-border/40 space-y-6">
{/* Cálculo por Peso */}
<div className="p-6 bg-primary/[0.03] rounded-3xl border border-primary/20 space-y-6">
<div className="flex items-center gap-3">
<div className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse"></div>
<span className="text-[10px] font-black text-primary uppercase tracking-[0.25em]">Cálculo por Peso (Kg)</span>
</div>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-1">
<span className="text-[9px] font-bold text-text-muted uppercase tracking-wider">Tinta Total</span>
<div className="text-3xl font-black text-primary leading-none">
{Math.round(selectedStudy.estimatedPaintVolume)} <span className="text-xs">L</span>
</div>
</div>
<div className="space-y-1">
<span className="text-[9px] font-bold text-text-muted uppercase tracking-wider">Diluente</span>
<div className="text-3xl font-black text-amber-500 leading-none">
{Math.round(selectedStudy.estimatedReducerVolume)} <span className="text-xs">L</span>
</div>
</div>
</div>
<div className="pt-6 border-t border-primary/10 space-y-4">
<div className="flex justify-between items-center">
<span className="text-[10px] font-bold text-text-muted uppercase">Taxa Média</span>
<span className="text-sm font-black text-text-main">
{selectedStudy.totalWeight > 0 ? ((selectedStudy.estimatedPaintVolume / selectedStudy.totalWeight).toFixed(2)) : '0.00'} <span className="text-[10px] text-text-muted">L/t</span>
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[10px] font-bold text-text-muted uppercase">Peso Total</span>
<span className="text-sm font-black text-primary">
{selectedStudy.totalWeight.toFixed(2)} TON
</span>
</div>
</div>
</div>
{/* Cálculo por Área (m²) - Condicional */}
{(selectedStudy.estimatedPaintVolumeByArea || 0) > 0 && (
<div className="p-6 bg-blue-500/[0.03] rounded-3xl border border-blue-500/20 space-y-6">
<div className="flex items-center gap-3">
<div className="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse"></div>
<span className="text-[10px] font-black text-blue-600 uppercase tracking-[0.25em]">Cálculo por Área (m²)</span>
</div>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-1">
<span className="text-[9px] font-bold text-text-muted uppercase tracking-wider">Tinta Total</span>
<div className="text-3xl font-black text-blue-600 leading-none">
{Math.round(selectedStudy.estimatedPaintVolumeByArea || 0)} <span className="text-xs">L</span>
</div>
</div>
<div className="space-y-1">
<span className="text-[9px] font-bold text-text-muted uppercase tracking-wider">Diluente</span>
<div className="text-3xl font-black text-amber-500 leading-none">
{Math.round(selectedStudy.estimatedReducerVolumeByArea || 0)} <span className="text-xs">L</span>
</div>
</div>
</div>
</div>
)}
<div className="grid grid-cols-2 gap-3 no-print">
{isAdmin() && (
<Button className="h-12 shadow-lg shadow-primary/20" onClick={handleSaveStudy} disabled={isSaving}>
<Save size={18} className="mr-2" />
{isSaving ? 'Salvando...' : 'Salvar'}
</Button>
)}
<Button variant="secondary" className="h-12 border-border/50" onClick={handlePrint}>
<Printer size={18} className="mr-2" />
PDF
</Button>
</div>
</div>
</Card>
</div>
{/* Main Content: Categories and Details */}
<div className="lg:col-span-8 space-y-8 print-col-12">
{/* Categorias */}
<Card className="p-8 border border-border/40 rounded-[32px] shadow-soft bg-surface">
<div className="flex justify-between items-center mb-8">
<div className="flex items-center gap-3">
<div className="p-2.5 bg-primary/10 rounded-xl text-primary">
<Weight size={20} />
</div>
<h3 className="font-black text-2xl text-text-main tracking-tight">Setores / Peças</h3>
</div>
{isAdmin() && (
<Button variant="ghost" className="text-primary hover:bg-primary/5 rounded-xl no-print font-black text-[10px] tracking-widest uppercase" onClick={addCategory}>
<Plus size={16} className="mr-2" /> Adicionar Categoria
</Button>
)}
</div>
<div className="space-y-6">
{selectedStudy.categories.map((cat) => (
<div key={cat.id} className="relative group/cat">
<div className="absolute -inset-0.5 bg-gradient-to-r from-primary/10 to-transparent rounded-[24px] opacity-0 group-hover/cat:opacity-100 transition-all"></div>
<div className="relative p-6 rounded-[24px] bg-surface-soft border border-border/40 hover:border-primary/20 transition-all">
<div className="flex flex-col md:flex-row gap-8">
<div className="flex-1 space-y-6">
<div className="flex justify-between items-center">
<div className="flex items-center gap-3 w-full">
<div className="w-2 h-8 bg-primary/20 rounded-full"></div>
<div className="flex-1">
<select
className="w-full bg-transparent border-none text-xl font-black text-text-main p-0 focus:ring-0 cursor-pointer appearance-none hover:text-primary transition-colors"
title="Selecione a Geometria"
value={cat.name}
onChange={e => {
const selectedName = e.target.value;
const geoType = geometryTypes.find(g => g.name === selectedName);
// Update name and auto-set efficiency (loss) if found
updateCategory(cat.id, {
name: selectedName,
...(geoType && { efficiency: geoType.efficiencyLoss || 20 })
});
}}
>
<option value="" disabled>Selecione a Geometria</option>
{geometryTypes.map(g => (
<option key={g.id || g._id} value={g.name} className="text-text-main font-bold">
{g.name}
</option>
))}
<option value={cat.name} className="text-text-muted italic">Manual: {cat.name}</option>
</select>
</div>
</div>
{isAdmin() && (
<button
onClick={() => removeCategory(cat.id)}
className="p-2 text-text-muted hover:text-error opacity-100 md:opacity-0 group-hover/cat:opacity-100 transition-all no-print"
title="Remover Categoria"
>
<Trash2 size={16} />
</button>
)}
</div>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<label className="block text-[9px] font-black text-text-muted uppercase tracking-widest">Peso Bruto (Ton)</label>
<div className="relative">
<Weight size={12} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
<input
type="number"
step="0.1"
className="w-full h-11 bg-surface border border-border/40 rounded-xl pl-9 pr-4 text-sm font-black outline-none focus:border-primary transition-all text-amber-500"
value={cat.weight}
onChange={e => updateCategory(cat.id, { weight: Number(e.target.value) })}
title="Peso Bruto em Toneladas"
placeholder="0.0"
/>
</div>
</div>
<div className="space-y-2">
<label className="block text-[9px] font-black text-text-muted uppercase tracking-widest">Taxa Hist. (L/t)</label>
<div className="relative">
<Droplet size={12} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
<input
type="number" step="1"
className="w-full h-11 bg-surface border border-border/40 rounded-xl pl-9 pr-4 text-sm font-black outline-none focus:border-primary transition-all text-amber-500"
value={cat.historicalYield}
onChange={e => updateCategory(cat.id, { historicalYield: Number(e.target.value) })}
title="Taxa Histórica L/t (litros por tonelada)"
placeholder="0"
/>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-6 mt-4">
<div className="space-y-2">
<label className="block text-[9px] font-black text-text-muted uppercase tracking-widest">Área Total (m²)</label>
<div className="relative">
<Ruler size={12} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
<input
type="number"
step="10"
className="w-full h-11 bg-surface border border-border/40 rounded-xl pl-9 pr-4 text-sm font-black outline-none focus:border-primary transition-all text-blue-600"
value={cat.area ? Math.round(cat.area) : ''}
onChange={e => updateCategory(cat.id, { area: Number(e.target.value) })}
title="Área em m²"
placeholder="0"
/>
</div>
</div>
<div className="space-y-2">
<label className="block text-[9px] font-black text-text-muted uppercase tracking-widest">Efic.estim. (%)</label>
<div className="relative">
<Ruler size={12} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
<input
type="number"
className="w-full h-11 bg-surface border border-border/40 rounded-xl pl-9 pr-4 text-sm font-black outline-none focus:border-primary transition-all text-blue-600"
value={cat.efficiency}
onChange={e => updateCategory(cat.id, { efficiency: Number(e.target.value) })}
title="Percentual de Eficiência/Perda (Aplica-se apenas ao cálculo por Área)"
placeholder="20"
/>
</div>
</div>
</div>
</div>
<div className="md:w-40 p-6 bg-primary/[0.04] rounded-[24px] border border-primary/10 flex flex-col justify-center items-center group-hover/cat:bg-primary/5 transition-all">
<span className="text-[9px] font-black text-primary/70 uppercase tracking-widest mb-1 text-center">Consumo (Peso)</span>
<div className="text-xl font-black text-primary">
{cat.litrosPeso || '0.0'} <span className="text-xs">L</span>
</div>
{cat.litrosArea && cat.litrosArea > 0 && (
<div className="flex flex-col items-center mt-2 animate-in fade-in">
<div className="w-8 h-px bg-primary/20 my-2"></div>
<span className="text-[9px] font-black text-blue-600/70 uppercase tracking-widest mb-1 text-center">Consumo Est. (m²)</span>
<div className="text-xl font-black text-blue-600">
{cat.litrosArea} <span className="text-xs">L</span>
</div>
</div>
)}
<div className="w-8 h-1 bg-primary/20 rounded-full mt-4"></div>
</div>
</div>
</div>
</div>
))}
</div>
</Card>
{/* Visualizacaos */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<Card className="p-8 bg-surface border border-border/40 rounded-[32px] shadow-soft h-[450px]">
<h3 className="font-black text-xs text-text-muted uppercase tracking-[0.2em] mb-8 flex items-center gap-3">
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
Distribuição por Categoria (L)
</h3>
<div className="h-[320px] w-full">
<ResponsiveContainer width="100%" height="100%" debounce={50}>
<BarChart data={chartData} margin={{ bottom: 20, top: 10 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#E2E8F0" opacity={0.5} />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
fontSize={10}
tick={{ fill: '#94a3b8', fontWeight: 700 }}
/>
<YAxis
axisLine={false}
tickLine={false}
fontSize={10}
tick={{ fill: '#94a3b8' }}
/>
<Bar dataKey="litros" fill="#0EA5E9" radius={10} barSize={40} />
</BarChart>
</ResponsiveContainer>
</div>
</Card>
<Card className="p-8 bg-surface border border-border/40 rounded-[32px] shadow-soft h-[450px] flex flex-col">
<h3 className="font-black text-xs text-text-muted uppercase tracking-[0.2em] mb-8 flex items-center gap-3">
<div className="w-2 h-2 rounded-full bg-primary animate-pulse"></div>
Projeção de Desvios
</h3>
<div className="h-[250px] w-full flex-1">
<ResponsiveContainer width="100%" height="100%" debounce={50}>
<ComposedChart data={projectionData} margin={{ bottom: 40, left: 10, right: 10, top: 10 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#E2E8F0" opacity={0.5} />
<XAxis
dataKey="dft"
axisLine={false}
tickLine={false}
fontSize={11}
tick={{ fill: '#94a3b8', fontWeight: 'bold' }}
label={{ value: 'Alvo (µm)', position: 'bottom', offset: 20, fontSize: 10, fontWeight: 900, fill: '#64748b', letterSpacing: '0.1em' }}
/>
<YAxis
axisLine={false}
tickLine={false}
fontSize={10}
tick={{ fill: '#94a3b8' }}
label={{ value: 'Vol. (L)', angle: -90, position: 'insideLeft', offset: 10, fontSize: 10, fill: '#94a3b8' }}
/>
<Tooltip
contentStyle={{ borderRadius: '20px', border: 'none', backgroundColor: '#1e293b', boxShadow: '0 20px 25px -5px rgb(0 0 0 / 0.3)', padding: '16px', color: '#fff' }}
formatter={(value) => [`${value} L`, 'Consumo']}
/>
<Area type="monotone" dataKey="vol" fill="#F59E0B" fillOpacity={0.1} stroke="#F59E0B" strokeWidth={4} dot={{ r: 5, fill: '#F59E0B', strokeWidth: 3, stroke: '#fff' }} activeDot={{ r: 7 }} />
</ComposedChart>
</ResponsiveContainer>
</div>
<div className="mt-6 p-4 bg-amber-500/5 rounded-2xl border border-amber-500/10 flex gap-4 items-start">
<AlertTriangle size={20} className="text-amber-500 shrink-0" />
<p className="text-[10px] text-amber-600/80 font-bold leading-relaxed uppercase tracking-tight">
Note que espessuras elevadas reduzem drasticamente o rendimento real, podendo exigir até 30% mais tinta do que o previsto.
</p>
</div>
</Card>
</div>
</div>
</div>
)}
{/* Template de Impressão (PDF/Relatório) - Visível apenas na impressão */}
{selectedStudy && (
<div className="print-report-container bg-white text-black p-0 min-h-screen font-sans">
{/* Cabeçalho do Relatório */}
<div className="flex justify-between items-start border-b-2 border-black pb-4 mb-6">
<div className="flex gap-6 items-center">
<div className="w-16 h-16 border-2 border-black rounded-xl flex items-center justify-center font-black text-2xl">TS</div>
<div>
<h1 className="text-2xl font-black uppercase tracking-tighter">Estudo de Rendimento Analítico</h1>
<p className="text-[10px] font-bold text-gray-600 uppercase tracking-widest">Previsão de consumo de tinta e projeção de produtividade</p>
</div>
</div>
<div className="text-right space-y-1">
<div className="text-[10px] font-black uppercase tracking-wider">Data: {format(new Date(), 'dd / MM / yyyy')}</div>
<div className="text-[10px] font-black uppercase tracking-wider underline flex gap-2 justify-end">
Responsável: <span className="w-32 h-3 border-b border-black inline-block"></span>
</div>
<div className="inline-block border-2 border-black rounded-lg px-3 py-1 text-[9px] font-black uppercase tracking-widest mt-2">
{selectedStudy.name.toUpperCase()}
</div>
</div>
</div>
{/* Cards de Resumo (Estilo do Relatório de Obras) */}
<div className="grid grid-cols-4 gap-4 mb-8">
<div className="border border-gray-300 rounded-xl p-4 space-y-1">
<span className="text-[8px] font-black text-gray-500 uppercase tracking-widest">Peso Total (Ton)</span>
<div className="text-xl font-black">{selectedStudy.totalWeight.toFixed(2)}</div>
<p className="text-[7px] text-gray-400 font-bold uppercase">Soma das categorias</p>
</div>
<div className="border border-gray-300 rounded-xl p-4 space-y-1">
<span className="text-[8px] font-black text-gray-500 uppercase tracking-widest">Tinta Total (Peso)</span>
<div className="text-xl font-black text-amber-600">{Math.round(selectedStudy.estimatedPaintVolume)} <span className="text-[10px]">L</span></div>
<p className="text-[7px] text-gray-400 font-bold uppercase">Baseado em L/t</p>
</div>
<div className="border border-gray-300 rounded-xl p-4 space-y-1">
<span className="text-[8px] font-black text-gray-500 uppercase tracking-widest">Tinta Total (Área)</span>
<div className="text-xl font-black text-blue-600">{Math.round(selectedStudy.estimatedPaintVolumeByArea || 0)} <span className="text-[10px]">L</span></div>
<p className="text-[7px] text-gray-400 font-bold uppercase">Baseado em m²</p>
</div>
<div className="border border-gray-300 rounded-xl p-4 space-y-1 border-black bg-gray-50">
<span className="text-[8px] font-black text-gray-500 uppercase tracking-widest">Taxa Média</span>
<div className="text-xl font-black">{selectedStudy.totalWeight > 0 ? (selectedStudy.estimatedPaintVolume / selectedStudy.totalWeight).toFixed(2) : '0.00'} <span className="text-[10px]">L/t</span></div>
<p className="text-[7px] text-gray-400 font-bold uppercase">Rendimento Global</p>
</div>
</div>
{/* Tabela de Detalhes */}
<div className="mb-8">
<div className="flex justify-between items-end mb-4 border-b border-gray-200 pb-2">
<h2 className="text-xs font-black uppercase tracking-[0.2em]">Detalhamento por Categoria</h2>
<p className="text-[8px] text-gray-500 font-bold uppercase">Visão analítica por geometria, peso e área</p>
</div>
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b-2 border-black">
<th className="py-2 text-[9px] font-black uppercase tracking-widest">Categoria / Tipo</th>
<th className="py-2 text-[9px] font-black uppercase tracking-widest text-center text-amber-600">Peso (T)</th>
<th className="py-2 text-[9px] font-black uppercase tracking-widest text-center text-blue-600">Área (m²)</th>
<th className="py-2 text-[9px] font-black uppercase tracking-widest text-center text-amber-600">Taxa (L/t)</th>
<th className="py-2 text-[9px] font-black uppercase tracking-widest text-center text-blue-600">Efic. (%)</th>
<th className="py-2 text-[9px] font-black uppercase tracking-widest text-right">Consumo (L)</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{selectedStudy.categories.map((cat, idx) => (
<tr key={cat.id || idx} className="border-b border-gray-100 italic">
<td className="py-3 pr-4">
<div className="text-[11px] font-black text-gray-800">{cat.name}</div>
</td>
<td className="py-3 text-center text-[10px] font-bold text-amber-700">{cat.weight.toFixed(2)}</td>
<td className="py-3 text-center text-[10px] font-bold text-blue-700">{cat.area ? Math.round(cat.area) : '--'}</td>
<td className="py-3 text-center text-[10px] font-bold text-amber-700">{cat.historicalYield}</td>
<td className="py-3 text-center text-[10px] font-bold text-blue-700">{cat.efficiency}%</td>
<td className="py-3 text-right">
<div className="text-[10px] font-black text-amber-700">{cat.litrosPeso} L <span className="text-[7px] uppercase font-bold text-gray-400 opacity-50">(p)</span></div>
{cat.litrosArea && <div className="text-[10px] font-black text-blue-700">{cat.litrosArea} L <span className="text-[7px] uppercase font-bold text-gray-400 opacity-50">(a)</span></div>}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Parâmetros e Projeção */}
<div className="grid grid-cols-2 gap-8 mb-8">
<div>
<h3 className="text-[10px] font-black uppercase tracking-widest mb-4 border-b border-gray-200 pb-2">Especificações Técnicas</h3>
<div className="space-y-3">
<div className="flex justify-between border-b border-dashed border-gray-200 pb-1">
<span className="text-[8px] font-bold text-gray-500 uppercase">Tinta Selecionada</span>
<span className="text-[10px] font-black">{findSheet(selectedStudy.dataSheetId)?.name || 'N/A'}</span>
</div>
<div className="flex justify-between border-b border-dashed border-gray-200 pb-1">
<span className="text-[8px] font-bold text-gray-500 uppercase text-blue-600">Espessura Alvo (DFT)</span>
<span className="text-[10px] font-black text-blue-700">{selectedStudy.targetDft} µm</span>
</div>
<div className="flex justify-between border-b border-dashed border-gray-200 pb-1">
<span className="text-[8px] font-bold text-gray-500 uppercase">Sólidos por Volume (SV)</span>
<span className="text-[10px] font-black">
{(() => {
let sv = findSheet(selectedStudy.dataSheetId)?.solidsVolume || 60;
if (sv <= 1) sv *= 100;
return sv.toFixed(0);
})()}%
</span>
</div>
<div className="flex justify-between border-b border-dashed border-gray-200 pb-1">
<span className="text-[8px] font-bold text-gray-500 uppercase">Diluição Aplicada</span>
<span className="text-[10px] font-black">{selectedStudy.dilutionPercent}%</span>
</div>
<div className="flex justify-between border-b border-dashed border-gray-200 pb-1">
<span className="text-[8px] font-bold text-gray-500 uppercase text-amber-600">Película Úmida (WFT)</span>
<span className="text-[10px] font-black text-amber-700">
{(() => {
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
</span>
</div>
</div>
</div>
<div className="bg-gray-50 rounded-2xl p-4 border border-gray-200">
<h3 className="text-[10px] font-black uppercase tracking-widest mb-2">Observações de Estudo</h3>
<p className="text-[8px] text-gray-600 font-medium leading-relaxed italic">
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.
</p>
<div className="mt-4 flex gap-2 items-start opacity-70">
<AlertTriangle size={14} className="text-gray-400 mt-1 shrink-0" />
<p className="text-[7px] text-gray-500 font-black uppercase tracking-tighter">
Variações de 10-15% no consumo real são consideradas normais dentro dos padrões industriais de pintura.
</p>
</div>
</div>
</div>
{/* Rodapé do Relatório */}
<div className="mt-auto pt-8 border-t border-gray-200">
<div className="flex justify-between items-center mb-8">
<div className="text-[8px] text-gray-400 font-black uppercase tracking-wider">
Gerado em: {format(new Date(), 'dd/MM/yyyy')} às {format(new Date(), 'HH:mm')}h<br />
ID do Estudo: {selectedStudy.id}
</div>
<div className="flex gap-12">
<div className="flex flex-col items-center">
<div className="w-48 border-b border-black h-8 mb-1"></div>
<span className="text-[7px] font-black uppercase tracking-widest text-gray-500">Assinatura do Responsável</span>
</div>
</div>
</div>
<div className="text-[7px] text-center text-gray-400 font-bold uppercase tracking-[0.3em]">
SteelPaint - Sistema de Gestão de Pintura Industrial (GPI)
</div>
</div>
</div>
)}
</div>
);
};