Files
ev-pwa/Dashboard copy.jsx
2025-06-17 17:46:21 +01:00

282 lines
9.8 KiB
JavaScript

import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Play,
Square,
Zap,
BatteryCharging,
Clock,
Activity,
Loader2,
ArrowLeft,
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { SectionCard } from '@/components/ui/SectionCard';
import { getAuthHeader } from '@/utils/apiAuthHeader';
import { useToast } from '@/components/ToastContext';
export default function ChargerDashboard() {
const { chargerId } = useParams(); // Obter o chargerId da URL
const [dados, setDados] = useState(null);
const [ampLimit, setAmpLimit] = useState(6);
const [maxAmps, setMaxAmps] = useState(32);
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const navigate = useNavigate();
const showToast = useToast();
// Buscar status do carregador
useEffect(() => {
if (!chargerId) {
setError("Carregador não encontrado!");
setLoading(false);
return;
}
let cancelado = false;
async function fetchStatus() {
setLoading(true);
setError('');
try {
const res = await fetch(`/api/chargers/${chargerId}/status`, {
headers: getAuthHeader(),
});
const json = await res.json();
if (!res.ok || !json.success) throw new Error(json.message || 'Erro ao obter status');
if (!cancelado) {
setDados(json.data);
setAmpLimit(json.data.ampLimit ?? 6);
setMaxAmps(json.data.maxAmps ?? 32);
}
} catch (err) {
if (!cancelado) setError(err.message || 'Falha ao buscar status');
} finally {
if (!cancelado) setLoading(false);
}
}
fetchStatus();
const interval = setInterval(fetchStatus, 8000);
return () => {
cancelado = true;
clearInterval(interval);
};
}, [chargerId]); // Atualizar a requisição sempre que o chargerId mudar
// Enviar comandos (iniciar/parar carregamento)
async function handleAction(action) {
if (!chargerId) return; // Adicionar verificação extra para chargerId
setBusy(true);
try {
const res = await fetch(`/api/chargers/${chargerId}/${action}`, {
method: 'POST',
headers: {
...getAuthHeader(),
'Content-Type': 'application/json',
},
body: JSON.stringify({ ampLimit }),
});
const json = await res.json();
if (!res.ok || !json.success) throw new Error(json.message || 'Erro ao enviar comando');
setDados(json.data);
showToast(
action === 'start' ? 'Carregamento iniciado' : 'Carregamento parado',
'success'
);
setError('');
} catch (err) {
setError(err.message);
showToast(err.message, 'error');
} finally {
setBusy(false);
}
}
return (
<div className="max-w-2xl mx-auto p-4 pb-24">
<button
className="mb-2 flex items-center gap-2 text-blue-600 hover:underline focus:outline-none"
onClick={() => navigate(-1)}
>
<ArrowLeft size={18} /> Voltar
</button>
<SectionCard
title={dados?.nome ? `Carregador: ${dados.nome}` : 'Carregador'}
icon={<Zap className="text-blue-600" />}
>
{error && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-red-500 text-sm mb-4"
aria-live="polite"
>
{error}
</motion.p>
)}
{(loading || busy) ? (
<div className="flex flex-col items-center justify-center py-8">
<Loader2 className="animate-spin text-blue-500" size={40} />
<span className="text-blue-600 mt-2">
{busy ? 'Enviando comando...' : 'Carregando status...'}
</span>
</div>
) : (
dados && (
<>
{/* Potência em destaque */}
<AnimatePresence mode="wait">
<motion.div
key={dados.potenciaAtual}
initial={{ scale: 0.85, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.85, opacity: 0 }}
transition={{ duration: 0.5 }}
className="relative w-40 h-40 mx-auto mb-8 select-none"
>
<div
className={`absolute inset-0 rounded-full border-8
${dados.potenciaAtual > 7
? 'border-yellow-400 opacity-60'
: 'border-blue-200 opacity-30'}`}
/>
<div
className={`absolute inset-2 rounded-full border-8
${dados.currentPower > 7
? 'border-yellow-500'
: 'border-blue-600'}
flex flex-col items-center justify-center text-blue-700`}
>
<motion.div
initial={{ rotate: -10 }}
animate={{ rotate: [0, 12, -12, 0] }}
transition={{
duration: 1,
repeat: Infinity,
repeatType: 'mirror',
ease: 'easeInOut',
}}
className={`mb-1 ${
dados.currentPower > 7
? 'text-yellow-500'
: 'text-blue-600'
}`}
>
<Zap size={32} />
</motion.div>
<span className="text-4xl font-extrabold drop-shadow">
{dados.currentPower} kW
</span>
<span className="text-xs text-gray-400 font-medium mt-1 tracking-wide">
Potência
</span>
</div>
</motion.div>
</AnimatePresence>
{/* Informações detalhadas */}
<div className="grid gap-5 text-sm text-gray-700 mb-8">
<InfoItem
icon={<BatteryCharging className="text-blue-500" />}
label="Estado"
value={dados.status}
valueClass="text-blue-600"
/>
<InfoItem
icon={<Clock className="text-gray-500" />}
label="Tempo de carregamento"
value={dados.timeRemaining}
/>
<InfoItem
icon={<Zap className="text-yellow-500" />}
label="Potência"
value={
<span className="text-lg font-bold text-yellow-600">
{dados.potenciaAtual} kW
</span>
}
/>
<InfoItem
icon={<Activity className="text-indigo-500" />}
label="Modo"
value={dados.modo?.toUpperCase()}
valueClass="text-indigo-600 font-bold"
/>
</div>
{/* Slider para Amperagem */}
<div className="mb-10">
<label
htmlFor="amp-range"
className="flex items-center gap-3 text-base font-medium text-gray-800 mb-4"
>
<Zap className="text-gray-600" size={20} />
Corrente máxima:{' '}
<span className="text-blue-700 font-semibold">
{ampLimit} A
</span>
</label>
<input
id="amp-range"
type="range"
min={6}
max={maxAmps}
value={ampLimit}
onChange={(e) => setAmpLimit(Number(e.target.value))}
className="w-full accent-blue-600"
aria-valuenow={ampLimit}
aria-valuemin={6}
aria-valuemax={maxAmps}
disabled={busy}
/>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>6 A</span>
<span>{maxAmps} A</span>
</div>
</div>
{/* Botões */}
<div className="flex justify-center gap-4 mt-8">
<motion.button
whileTap={{ scale: 0.95 }}
onClick={() => handleAction('start')}
className="bg-green-600 hover:bg-green-700 text-white px-5 py-2 rounded-xl shadow font-medium transition flex items-center gap-2 active:scale-95 focus:outline-none focus:ring-2 focus:ring-green-400"
disabled={busy}
aria-label="Iniciar carregamento"
>
<Play size={18} /> Iniciar
</motion.button>
<motion.button
whileTap={{ scale: 0.95 }}
onClick={() => handleAction('stop')}
className="bg-red-500 hover:bg-red-600 text-white px-5 py-2 rounded-xl shadow font-medium transition flex items-center gap-2 active:scale-95 focus:outline-none focus:ring-2 focus:ring-red-400"
disabled={busy}
aria-label="Parar carregamento"
>
<Square size={18} /> Parar
</motion.button>
</div>
</>
)
)}
</SectionCard>
</div>
);
}
function InfoItem({ icon, label, value, valueClass = '' }) {
return (
<div className="grid grid-cols-2 items-center gap-2 py-1.5">
<div className="flex items-center gap-2 text-gray-600 font-medium">
{icon}
<span>{label}</span>
</div>
<div className={`text-right text-base font-semibold ${valueClass}`}>
{value}
</div>
</div>
);
}