1067 lines
69 KiB
TypeScript
1067 lines
69 KiB
TypeScript
|
||
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>
|
||
);
|
||
};
|