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

497 lines
31 KiB
TypeScript

import React, { useState, useMemo } from 'react';
import { Card } from '../components/Card';
import { Button } from '../components/Button';
import { Ruler, Droplets, CloudRain, PiggyBank, Paintbrush, ArrowRightLeft, Info, Calculator, Layers, HelpCircle } from 'lucide-react';
import { clsx } from 'clsx';
interface InputGroupProps {
label: React.ReactNode;
value: string;
onChange: (value: string) => void;
placeholder?: string;
unit?: string;
}
const InputGroup: React.FC<InputGroupProps> = ({ label, value, onChange, placeholder, unit }) => (
<div className="space-y-1">
<label className="text-[10px] font-bold text-primary uppercase tracking-[0.15em]">{label}</label>
<div className="relative">
<input
type="number"
className="w-full bg-surface-soft border border-border/40 rounded-xl px-4 py-2.5 text-text-main font-bold focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all placeholder:text-text-muted/30 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
{unit && <span className="absolute right-4 top-1/2 -translate-y-1/2 text-xs font-bold text-text-muted">{unit}</span>}
</div>
</div>
);
export const CalculatorDashboard: React.FC = () => {
const [activeTab, setActiveTab] = useState('conversion');
// 1. Conversion
const [microns, setMicrons] = useState<string>('');
const [mils, setMils] = useState<string>('');
const handleMicronChange = (val: string) => {
setMicrons(val);
if (val) setMils((parseFloat(val) / 25.4).toFixed(2));
else setMils('');
};
const handleMilsChange = (val: string) => {
setMils(val);
if (val) setMicrons((parseFloat(val) * 25.4).toFixed(1));
else setMicrons('');
};
// 2. Thickness
const [epsParams, setEpsParams] = useState({ sv: '', dilution: '', wft: '' });
const epsResult = useMemo(() => {
const { sv, dilution, wft } = epsParams;
if (sv && wft) {
const svVal = parseFloat(sv);
const dilVal = parseFloat(dilution) || 0;
const wftVal = parseFloat(wft);
return (wftVal * svVal) / (100 + dilVal);
}
return null;
}, [epsParams]);
const [epuParams, setEpuParams] = useState({ sv: '', dilution: '', dft: '' });
const epuResult = useMemo(() => {
const { sv, dilution, dft } = epuParams;
if (sv && dft) {
const svVal = parseFloat(sv);
const dilVal = parseFloat(dilution) || 0;
const dftVal = parseFloat(dft);
return (dftVal * (100 + dilVal)) / svVal;
}
return null;
}, [epuParams]);
const [deadVolParams, setDeadVolParams] = useState({ roughness: '', sv: '', area: '' });
const deadVolResult = useMemo(() => {
const { roughness, sv, area } = deadVolParams;
if (roughness && sv && area) {
const rVal = parseFloat(roughness);
const svVal = parseFloat(sv);
const aVal = parseFloat(area);
const volDry = (aVal * rVal * 0.5) / 1000;
const volWet = volDry / (svVal / 100);
return { dry: volDry, wet: volWet };
}
return null;
}, [deadVolParams]);
// 3. Dew Point
const [envParams, setEnvParams] = useState({ temp: '', rh: '' });
const { dewPoint, dpStatus } = useMemo(() => {
const { temp, rh } = envParams;
if (temp && rh) {
const T = parseFloat(temp);
const RH = parseFloat(rh);
const a = 17.27;
const b = 237.7;
const alpha = ((a * T) / (b + T)) + Math.log(RH / 100);
const Td = (b * alpha) / (a - alpha);
const delta = T - Td;
let status = '';
if (delta < 3) status = 'Risco: Condensação iminente (Delta < 3°C)';
else status = 'Condição Segura (Delta > 3°C)';
return { dewPoint: Td, dpStatus: status };
}
return { dewPoint: null, dpStatus: '' };
}, [envParams]);
// 4. Consumption & Cost
const [consAreaParams, setConsAreaParams] = useState({ area: '', eps: '', sv: '', loss: '' });
const consAreaResult = useMemo(() => {
const { area, eps, sv, loss } = consAreaParams;
if (area && eps && sv) {
const a = parseFloat(area);
const e = parseFloat(eps);
const s = parseFloat(sv);
const l = parseFloat(loss) || 0;
const theo = (a * e) / (10 * s);
return theo / (1 - (l / 100));
}
return null;
}, [consAreaParams]);
const [consWeightParams, setConsWeightParams] = useState({ weight: '', relation: '' });
const consWeightResult = useMemo(() => {
const { weight, relation } = consWeightParams;
if (weight && relation) {
const w = parseFloat(weight);
const r = parseFloat(relation);
const tons = w / 1000;
return tons * r;
}
return null;
}, [consWeightParams]);
const [costParams, setCostParams] = useState({ coats: '1', area: '', eps: '', sv: '', dilution: '0', loss: '', price: '' });
const costResult = useMemo(() => {
const { coats, area, eps, sv, dilution, loss, price } = costParams;
if (area && eps && sv && price) {
const c = parseFloat(coats) || 1;
const a = parseFloat(area);
const e = parseFloat(eps);
const s = parseFloat(sv);
const d = parseFloat(dilution) || 0;
const l = parseFloat(loss) || 0;
const p = parseFloat(price);
const theoPaint = (a * e) / (10 * s);
const realPaintPerCoat = theoPaint / (1 - (l / 100));
const totalPaint = realPaintPerCoat * c;
const totalThinner = totalPaint * (d / 100);
const totalCost = totalPaint * p;
return { totalCost, totalPaint, totalThinner };
}
return null;
}, [costParams]);
// 5. Nozzles
const [nozzleCode, setNozzleCode] = useState('');
const [showNozzleHelp, setShowNozzleHelp] = useState(false);
const [nozzleResult, setNozzleResult] = useState<{ fan: number, flow: number, desc: string } | null>(null);
const calculateNozzle = () => {
if (nozzleCode.length < 3) return;
const widthDigit = parseInt(nozzleCode[0]);
const holeDigits = parseInt(nozzleCode.slice(1));
const fan = widthDigit * 5;
const flow = (holeDigits * holeDigits) * 0.0039;
let desc = '';
if (holeDigits < 15) desc = "Acabamento fino, líquidos leves (Verniz, Stain)";
else if (holeDigits < 19) desc = "Uso geral, média viscosidade (Látex, Esmalte)";
else if (holeDigits < 25) desc = "Alta produtividade, viscosidade alta (Epóxi, PU)";
else desc = "Extrema cobertura, materiais pesados (Massa, Ignífugo)";
setNozzleResult({ fan, flow, desc });
};
return (
<div className="space-y-8 animate-in fade-in duration-700">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6 pb-2">
<div className="space-y-4">
<div>
<h1 className="text-3xl md:text-4xl font-black text-text-main tracking-tight mb-0">Ferramentas & Cálculos</h1>
<p className="text-sm text-text-muted font-medium tracking-wide">Utilitários técnicos para pintura industrial</p>
</div>
</div>
</div>
{/* Tabs */}
<div className="flex items-center justify-between border-b border-border/40 scrollbar-hide overflow-x-auto">
<nav className="flex space-x-10 min-w-max px-2" aria-label="Tabs">
{[
{ id: 'conversion', label: 'Conversões', icon: ArrowRightLeft },
{ id: 'thickness', label: 'Espessura', icon: Ruler },
{ id: 'dewpoint', label: 'Ambiente', icon: CloudRain },
{ id: 'consumption', label: 'Consumo & Custo', icon: PiggyBank },
{ id: 'nozzles', label: 'Bicos Airless', icon: Paintbrush },
].map((tab) => {
const Icon = tab.icon;
const active = activeTab === tab.id;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={clsx(
'whitespace-nowrap py-5 px-1 border-b-2 font-bold text-xs uppercase tracking-[0.15em] flex items-center gap-3 transition-all relative',
active
? 'border-primary text-primary'
: 'border-transparent text-text-muted hover:text-text-main hover:border-border'
)}
>
<Icon className={clsx("w-4 h-4", active ? "text-primary" : "text-text-muted")} />
{tab.label}
{active && <span className="absolute bottom-[-1px] left-0 right-0 h-[2px] bg-primary shadow-[0_0_12px_rgba(13,127,242,0.6)]"></span>}
</button>
);
})}
</nav>
</div>
{/* Content for Tabs */}
<div className="min-h-[400px]">
{/* 1. CONVERSION */}
{activeTab === 'conversion' && (
<div className="max-w-2xl mx-auto space-y-6 animate-in slide-in-from-bottom-4 duration-500">
<Card className="p-8 space-y-8 bg-surface border-border/40 shadow-soft">
<div className="flex flex-col gap-2 border-b border-border/40 pb-6">
<h3 className="text-xl font-bold text-text-main">Conversor de Unidades</h3>
<p className="text-sm text-text-muted">Microns (μm) Milesimos de Polegada (mils)</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
<InputGroup
label={<>Microns (<span className="normal-case">μm</span>)</>}
unit="μm"
value={microns}
onChange={handleMicronChange}
placeholder="0"
/>
<div className="hidden md:flex justify-center text-text-muted"><ArrowRightLeft /></div>
<InputGroup label="Mils" unit="mils" value={mils} onChange={handleMilsChange} placeholder="0" />
</div>
<div className="bg-primary/5 rounded-xl p-4 flex gap-3 text-sm text-text-secondary">
<Info className="w-5 h-5 text-primary shrink-0" />
<p>Fator de conversão: <span className="font-bold text-text-main">1 mil = 25.4 μm</span>.</p>
</div>
</Card>
</div>
)}
{/* 2. THICKNESS */}
{activeTab === 'thickness' && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 animate-in slide-in-from-bottom-4 duration-500">
{/* EPS Calc */}
<Card className="p-6 space-y-6 border-border/40 hover:shadow-md transition-all">
<div className="flex items-center gap-3 pb-4 border-b border-border/40">
<div className="p-2 bg-blue-500/10 rounded-lg text-blue-500"><Ruler size={18} /></div>
<h3 className="font-bold text-text-main">Cálculo de EPS (Seca)</h3>
</div>
<div className="space-y-4">
<InputGroup label="Espessura Úmida (EPU)" unit="μm" value={epsParams.wft} onChange={(v) => setEpsParams({ ...epsParams, wft: v })} />
<InputGroup label="Sólidos por Volume" unit="%" value={epsParams.sv} onChange={(v) => setEpsParams({ ...epsParams, sv: v })} />
<InputGroup label="Diluição Adicionada" unit="%" value={epsParams.dilution} onChange={(v) => setEpsParams({ ...epsParams, dilution: v })} />
</div>
<div className="pt-4 mt-4 bg-surface-soft rounded-xl p-4 text-center">
<span className="text-[10px] uppercase font-bold text-text-muted">Resultado Esperado</span>
<div className="text-3xl font-black text-primary mt-1">{epsResult ? epsResult.toFixed(1) : '--'} <span className="text-sm text-text-muted">μm</span></div>
</div>
</Card>
{/* EPU Calc */}
<Card className="p-6 space-y-6 border-border/40 hover:shadow-md transition-all">
<div className="flex items-center gap-3 pb-4 border-b border-border/40">
<div className="p-2 bg-indigo-500/10 rounded-lg text-indigo-500"><Droplets size={18} /></div>
<h3 className="font-bold text-text-main">Cálculo de EPU (Úmida)</h3>
</div>
<div className="space-y-4">
<InputGroup label="EPS Desejada" unit="μm" value={epuParams.dft} onChange={(v) => setEpuParams({ ...epuParams, dft: v })} />
<InputGroup label="Sólidos por Volume" unit="%" value={epuParams.sv} onChange={(v) => setEpuParams({ ...epuParams, sv: v })} />
<InputGroup label="Diluição Prevista" unit="%" value={epuParams.dilution} onChange={(v) => setEpuParams({ ...epuParams, dilution: v })} />
</div>
<div className="pt-4 mt-4 bg-surface-soft rounded-xl p-4 text-center">
<span className="text-[10px] uppercase font-bold text-text-muted">Aplicar Camada de</span>
<div className="text-3xl font-black text-indigo-500 mt-1">{epuResult ? epuResult.toFixed(0) : '--'} <span className="text-sm text-text-muted">μm</span></div>
</div>
</Card>
{/* Dead Volume */}
<Card className="p-6 space-y-6 border-border/40 hover:shadow-md transition-all">
<div className="flex items-center gap-3 pb-4 border-b border-border/40">
<div className="p-2 bg-amber-500/10 rounded-lg text-amber-500"><Layers size={18} /></div>
<h3 className="font-bold text-text-main">Volume Morto</h3>
</div>
<div className="space-y-4">
<InputGroup label="Área Total" unit="m²" value={deadVolParams.area} onChange={(v) => setDeadVolParams({ ...deadVolParams, area: v })} />
<InputGroup label="Rugosidade Média" unit="μm" value={deadVolParams.roughness} onChange={(v) => setDeadVolParams({ ...deadVolParams, roughness: v })} />
<InputGroup label="Sólidos por Volume" unit="%" value={deadVolParams.sv} onChange={(v) => setDeadVolParams({ ...deadVolParams, sv: v })} />
</div>
<div className="pt-4 mt-4 bg-surface-soft rounded-xl p-4 text-center space-y-2">
<div>
<span className="text-[9px] uppercase font-bold text-text-muted">Volume Tinta (L)</span>
<div className="text-2xl font-black text-text-main mt-0.5">{deadVolResult ? deadVolResult.wet.toFixed(2) : '--'} <span className="text-xs text-text-muted">L</span></div>
</div>
</div>
</Card>
</div>
)}
{/* 3. DEW POINT */}
{activeTab === 'dewpoint' && (
<div className="max-w-xl mx-auto space-y-6 animate-in slide-in-from-bottom-4 duration-500">
<Card className="p-8 space-y-8 bg-surface border-border/40 shadow-soft">
<div className="flex flex-col gap-2 border-b border-border/40 pb-6">
<h3 className="text-xl font-bold text-text-main">Ponto de Orvalho</h3>
<p className="text-sm text-text-muted">Cálculo da temperatura de condensação</p>
</div>
<div className="grid grid-cols-2 gap-6">
<InputGroup label="Temp. Ambiente" unit="°C" value={envParams.temp} onChange={(v) => setEnvParams({ ...envParams, temp: v })} />
<InputGroup label="Umidade Relativa" unit="%" value={envParams.rh} onChange={(v) => setEnvParams({ ...envParams, rh: v })} />
</div>
<div className={clsx(
"rounded-xl p-6 text-center transition-all border",
dewPoint === null ? "bg-surface-soft border-transparent" :
(dpStatus.includes('Risco') ? "bg-error/10 border-error/30" : "bg-success/10 border-success/30")
)}>
<span className="text-[10px] uppercase font-bold text-text-muted block mb-2">Ponto de Orvalho Calculado</span>
<div className={clsx("text-4xl font-black mb-2", dpStatus.includes('Risco') ? "text-error" : "text-success")}>
{dewPoint ? dewPoint.toFixed(1) : '--'}°C
</div>
<span className={clsx("text-xs font-bold uppercase tracking-widest", dpStatus.includes('Risco') ? "text-error" : "text-success")}>
{dpStatus || 'Aguardando dados...'}
</span>
</div>
</Card>
</div>
)}
{/* 4. CONSUMPTION & COST */}
{activeTab === 'consumption' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 animate-in slide-in-from-bottom-4 duration-500">
{/* Area Consumption */}
<Card className="p-6 space-y-6 border-border/40">
<div className="flex items-center gap-3 pb-4 border-b border-border/40">
<div className="p-2 bg-emerald-500/10 rounded-lg text-emerald-500"><Paintbrush size={18} /></div>
<h3 className="font-bold text-text-main">Consumo por Área</h3>
</div>
<div className="grid grid-cols-2 gap-4">
<InputGroup label="Área" unit="m²" value={consAreaParams.area} onChange={(v) => setConsAreaParams({ ...consAreaParams, area: v })} />
<InputGroup label="EPS" unit="μm" value={consAreaParams.eps} onChange={(v) => setConsAreaParams({ ...consAreaParams, eps: v })} />
<InputGroup label="Sólidos Vol." unit="%" value={consAreaParams.sv} onChange={(v) => setConsAreaParams({ ...consAreaParams, sv: v })} />
<InputGroup label="Perdas" unit="%" value={consAreaParams.loss} onChange={(v) => setConsAreaParams({ ...consAreaParams, loss: v })} />
</div>
<div className="pt-4 mt-4 bg-surface-soft rounded-xl p-4 text-center">
<span className="text-[10px] uppercase font-bold text-text-muted">Consumo Estimado</span>
<div className="text-3xl font-black text-emerald-500 mt-1">{consAreaResult ? consAreaResult.toFixed(1) : '--'} <span className="text-sm text-text-muted">Litros</span></div>
</div>
</Card>
{/* Weight Consumption */}
<Card className="p-6 space-y-6 border-border/40">
<div className="flex items-center gap-3 pb-4 border-b border-border/40">
<div className="p-2 bg-orange-500/10 rounded-lg text-orange-500"><PiggyBank size={18} /></div>
<h3 className="font-bold text-text-main">Consumo por Peso</h3>
</div>
<div className="space-y-4">
<InputGroup label="Peso Total" unit="Kg" value={consWeightParams.weight} onChange={(v) => setConsWeightParams({ ...consWeightParams, weight: v })} />
<InputGroup label="Relação" unit="L/Ton" value={consWeightParams.relation} onChange={(v) => setConsWeightParams({ ...consWeightParams, relation: v })} />
</div>
<div className="pt-4 mt-8 bg-surface-soft rounded-xl p-4 text-center">
<span className="text-[10px] uppercase font-bold text-text-muted">Volume Necessário</span>
<div className="text-3xl font-black text-orange-500 mt-1">{consWeightResult ? consWeightResult.toFixed(1) : '--'} <span className="text-sm text-text-muted">Litros</span></div>
</div>
</Card>
{/* Cost Estimator */}
<Card className="p-6 space-y-6 border-border/40 lg:col-span-2">
<div className="flex items-center gap-3 pb-4 border-b border-border/40">
<div className="p-2 bg-primary/10 rounded-lg text-primary"><Calculator size={18} /></div>
<h3 className="font-bold text-text-main">Estimativa de Custos</h3>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<InputGroup label="Demãos" unit="un" value={costParams.coats} onChange={(v) => setCostParams({ ...costParams, coats: v })} />
<InputGroup label="Área" unit="m²" value={costParams.area} onChange={(v) => setCostParams({ ...costParams, area: v })} />
<InputGroup label="EPS" unit="μm" value={costParams.eps} onChange={(v) => setCostParams({ ...costParams, eps: v })} />
<InputGroup label="Preço" unit="R$/L" value={costParams.price} onChange={(v) => setCostParams({ ...costParams, price: v })} />
<InputGroup label="Sólidos Vol." unit="%" value={costParams.sv} onChange={(v) => setCostParams({ ...costParams, sv: v })} />
<InputGroup label="Perdas" unit="%" value={costParams.loss} onChange={(v) => setCostParams({ ...costParams, loss: v })} />
<InputGroup label="Diluição" unit="%" value={costParams.dilution} onChange={(v) => setCostParams({ ...costParams, dilution: v })} />
</div>
<div className="pt-4 mt-4 grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-surface-soft rounded-xl p-4 text-center">
<span className="text-[9px] uppercase font-bold text-text-muted">Volume Tinta</span>
<div className="text-xl font-black text-text-main">{costResult ? costResult.totalPaint.toFixed(1) : '--'} L</div>
</div>
<div className="bg-surface-soft rounded-xl p-4 text-center">
<span className="text-[9px] uppercase font-bold text-text-muted">Volume Diluente</span>
<div className="text-xl font-black text-text-main">{costResult ? costResult.totalThinner.toFixed(1) : '--'} L</div>
</div>
<div className="bg-primary/10 rounded-xl p-4 text-center border border-primary/20">
<span className="text-[9px] uppercase font-bold text-text-muted">Custo Estimado</span>
<div className="text-2xl font-black text-primary">R$ {costResult ? costResult.totalCost.toFixed(2) : '--'}</div>
</div>
</div>
</Card>
</div>
)}
{/* 5. Nozzles */}
{activeTab === 'nozzles' && (
<div className="max-w-xl mx-auto space-y-6 animate-in slide-in-from-bottom-4 duration-500">
<Card className="p-8 space-y-8 bg-surface border-border/40 shadow-soft">
<div className="flex flex-col gap-2 border-b border-border/40 pb-6">
<h3 className="text-xl font-bold text-text-main">Seletor de Bicos Airless</h3>
<p className="text-sm text-text-muted">Insira o código do bico (ex: 517) para ver detalhes</p>
</div>
<div className="flex gap-4 items-end">
<div className="flex-1">
<InputGroup label="Código do Bico" placeholder="ex: 517" value={nozzleCode} onChange={setNozzleCode} />
</div>
<button
onClick={() => setShowNozzleHelp(!showNozzleHelp)}
className="mb-[2px] p-3 rounded-xl bg-surface-soft text-text-muted hover:text-primary hover:bg-primary/10 transition-colors"
title="Como ler o código?"
>
<HelpCircle size={20} />
</button>
<Button onClick={calculateNozzle} className="mb-[2px]">Calcular</Button>
</div>
{showNozzleHelp && (
<div className="bg-surface-soft border border-border/40 rounded-xl p-4 text-sm space-y-3 animate-in fade-in zoom-in-95 duration-200">
<h4 className="font-bold text-text-main flex items-center gap-2">
<Info size={16} className="text-primary" />
Entendendo o Código (ex: 517)
</h4>
<ul className="space-y-2 text-text-secondary">
<li className="flex gap-2">
<span className="font-black text-primary shrink-0">1º Dígito</span>
<div>
<strong className="text-text-main">Ângulo do Leque:</strong> Multiplique por 5 para saber a largura em cm (aprox a 30cm da superfície).
<br /><span className="text-xs text-text-muted">Ex: 5xx = 50° (aprox. 25cm).</span>
</div>
</li>
<li className="flex gap-2">
<span className="font-black text-primary shrink-0">Últimos</span>
<div>
<strong className="text-text-main">Orifício (Vazão):</strong> Diâmetro em milésimos de polegada. Quanto maior, mais tinta sai.
<br /><span className="text-xs text-text-muted">Ex: x17 = 0.017". Indicado para látex/esmalte.</span>
</div>
</li>
</ul>
</div>
)}
{nozzleResult && (
<div className="bg-surface-soft border border-border/20 rounded-2xl p-6 space-y-6">
<div className="grid grid-cols-2 gap-8 text-center">
<div>
<span className="text-[10px] font-bold text-text-muted uppercase tracking-widest block mb-1">Abertura Leque</span>
<span className="text-3xl font-black text-text-main">{nozzleResult.fan} <span className="text-sm text-text-muted font-bold">cm</span></span>
<p className="text-[10px] text-text-muted mt-1">(Aprox. a 30cm)</p>
</div>
<div>
<span className="text-[10px] font-bold text-text-muted uppercase tracking-widest block mb-1">Vazão Aprox.</span>
<span className="text-3xl font-black text-primary">{nozzleResult.flow.toFixed(2)} <span className="text-sm text-text-muted font-bold">L/min</span></span>
<p className="text-[10px] text-text-muted mt-1">(@ 2000 psi)</p>
</div>
</div>
<div className="pt-4 border-t border-border/20">
<p className="text-center text-sm font-medium text-text-secondary">{nozzleResult.desc}</p>
</div>
</div>
)}
<div className="bg-primary/5 rounded-xl p-4 flex gap-3 text-sm text-text-secondary">
<Info className="w-5 h-5 text-primary shrink-0" />
<p className="text-xs">Valores teóricos de referência. Consulte sempre a ficha técnica do fabricante do equipamento.</p>
</div>
</Card>
</div>
)}
</div>
</div>
);
};