Compare commits
11 Commits
94f012f417
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 76d852b2ae | |||
| 3e7be044cf | |||
| 37fa90cc81 | |||
| 6a2a793797 | |||
| 6ef65564db | |||
| 1a8b5b21b1 | |||
| ca733ba483 | |||
| 4530dcc970 | |||
| 8311cd4f6a | |||
| 56349ccc39 | |||
| 3a1eb36981 |
11
index.html
11
index.html
@@ -4,12 +4,19 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/src/index.css" />
|
||||
<!-- <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet"> -->
|
||||
<!-- Remover link para tailwind.min.css -->
|
||||
<!-- <link href="/tailwind.min.css" rel="stylesheet"> -->
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Arial', 'Helvetica', 'sans-serif'; /* Usando fontes genéricas do sistema */
|
||||
}
|
||||
</style>
|
||||
<title>Vite + React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<!-- Adicionar o link para o seu arquivo CSS com o Tailwind configurado -->
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
4227
package-lock.json
generated
4227
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,10 +21,15 @@
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"live-server": "^1.2.2",
|
||||
"postcss": "^8.5.4",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
6
postcss.config.cjs
Executable file
6
postcss.config.cjs
Executable file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
13
src/components/Alert.jsx
Executable file
13
src/components/Alert.jsx
Executable file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Alert({ type = 'info', children }) {
|
||||
const base = 'p-2 mb-2 rounded';
|
||||
const types = {
|
||||
success: 'bg-green-600 text-white',
|
||||
error: 'bg-red-600 text-white',
|
||||
info: 'bg-gray-200',
|
||||
};
|
||||
return (
|
||||
<div className={`${base} ${types[type] || types.info}`}>{children}</div>
|
||||
);
|
||||
}
|
||||
@@ -9,21 +9,24 @@ const Navbar = () => {
|
||||
setIsMenuOpen(!isMenuOpen); // Alterna o estado do menu (aberto/fechado)
|
||||
};
|
||||
|
||||
const handleLinkClick = () => {
|
||||
setIsMenuOpen(false); // Fecha o menu quando um link é clicado
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="navbar">
|
||||
<div className="navbar-container">
|
||||
<div className="navbar-logo">
|
||||
</div>
|
||||
<ul className={`navbar-links ${isMenuOpen ? 'active' : ''}`}>
|
||||
<li><Link to="/dashboard">Dashboard</Link></li>
|
||||
<li><Link to="/settings">Settings</Link></li>
|
||||
<li><Link to="/security">Security</Link></li>
|
||||
<li><Link to="/connectivity">Connectivity</Link></li>
|
||||
<li><Link to="/ocpp">OCPP</Link></li>
|
||||
<li><Link to="/electrical-network">Rede Elétrica</Link></li>
|
||||
<nav className="bg-gradient-to-r from-green-700 to-green-600 text-white p-4 shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-bold text-xl">EVSE</div>
|
||||
<ul className={`flex-col md:flex-row md:flex gap-4 ${isMenuOpen ? 'flex' : 'hidden'} md:!flex`}>
|
||||
<li><Link className="hover:underline" to="/dashboard" onClick={handleLinkClick}>Dashboard</Link></li>
|
||||
<li><Link className="hover:underline" to="/settings" onClick={handleLinkClick}>Settings</Link></li>
|
||||
<li><Link className="hover:underline" to="/security" onClick={handleLinkClick}>Security</Link></li>
|
||||
<li><Link className="hover:underline" to="/connectivity" onClick={handleLinkClick}>Connectivity</Link></li>
|
||||
<li><Link className="hover:underline" to="/ocpp" onClick={handleLinkClick}>OCPP</Link></li>
|
||||
<li><Link className="hover:underline" to="/electrical-network" onClick={handleLinkClick}>Rede Elétrica</Link></li>
|
||||
</ul>
|
||||
<button className="menu-icon" onClick={toggleMenu}>
|
||||
☰ {/* Ícone do menu hamburguer */}
|
||||
<button className="md:hidden text-3xl" onClick={toggleMenu}>
|
||||
☰
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// src/components/PageLayout.jsx
|
||||
export default function PageLayout({ title, children }) {
|
||||
return (
|
||||
<div className="page-container">
|
||||
<h1>{title}</h1>
|
||||
<div className="max-w-3xl mx-auto p-5 bg-white rounded shadow">
|
||||
<h1 className="text-2xl font-bold mb-5">{title}</h1>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
679
src/index.css
679
src/index.css
@@ -1,676 +1,3 @@
|
||||
/* index.css */
|
||||
|
||||
/* Paleta de cores e variáveis globais */
|
||||
:root {
|
||||
--color-primary: #4CAF50;
|
||||
--color-primary-dark: #45a049;
|
||||
--color-dark: #333333;
|
||||
--color-light: #ffffff;
|
||||
--color-background: #f4f4f9;
|
||||
--color-background-dark: #e8e8ef;
|
||||
--color-surface: #ffffff;
|
||||
--color-table-header: #f2f2f2;
|
||||
--color-border: #dddddd;
|
||||
--color-info: #2196F3;
|
||||
--color-info-dark: #1e88e5;
|
||||
--color-alert: #f44336;
|
||||
--color-alert-dark: #e53935;
|
||||
--color-text-muted: #555555;
|
||||
--font-base: 'Roboto', Arial, sans-serif;
|
||||
--border-radius: 5px;
|
||||
--transition-fast: 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Resetando margens e padding */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Fonte padrão para o projeto */
|
||||
body {
|
||||
font-family: var(--font-base);
|
||||
background: linear-gradient(var(--color-background), var(--color-background-dark));
|
||||
color: var(--color-dark);
|
||||
line-height: 1.6;
|
||||
font-size: 16px; /* Tamanho de fonte confortável */
|
||||
}
|
||||
|
||||
/* Definindo um fundo geral para o layout */
|
||||
.container {
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Estilos básicos de links */
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit; /* Cor do link será herdada do texto */
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
/* Estilos para os títulos */
|
||||
h1, h2, h3 {
|
||||
font-weight: bold;
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
/* Estilo básico de botões */
|
||||
button {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-light);
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius);
|
||||
transition: background-color var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--color-primary-dark);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Tabelas */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background-color: var(--color-surface);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--color-table-header);
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
/* Estilo de caixas de alerta */
|
||||
.alert {
|
||||
background-color: var(--color-alert);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
/* Estilos para o Dashboard */
|
||||
.dashboard-container {
|
||||
background-color: var(--color-surface);
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Estilos para Settings */
|
||||
.settings-container {
|
||||
background-color: var(--color-surface);
|
||||
padding: 20px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.settings-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.settings-item label {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.settings-item input[type="range"] {
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.settings-item input[type="number"] {
|
||||
width: 100px;
|
||||
padding: 5px;
|
||||
font-size: 16px;
|
||||
margin-left: 10px;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* Estilo do container do slider */
|
||||
.slider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.slider-container span {
|
||||
font-size: 16px;
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
/* Botão de salvar */
|
||||
button.save-button {
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius);
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
button.save-button:hover {
|
||||
background-color: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
/* Responsividade para telas pequenas */
|
||||
@media (max-width: 768px) {
|
||||
.settings-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.settings-item input[type="number"] {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
button.save-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Estilo para o Título */
|
||||
.settings-title {
|
||||
font-size: 32px; /* Tamanho maior para maior destaque */
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
text-align: center; /* Centralizado */
|
||||
color: var(--color-dark);
|
||||
text-transform: uppercase; /* Texto em maiúsculo para chamar atenção */
|
||||
}
|
||||
|
||||
/* Ajustando os parâmetros do título no mobile */
|
||||
@media (max-width: 768px) {
|
||||
.settings-title {
|
||||
font-size: 28px;
|
||||
margin-bottom: 20px; /* Menor espaçamento em telas pequenas */
|
||||
}
|
||||
}
|
||||
|
||||
/* Navbar Estilo */
|
||||
.navbar {
|
||||
background: linear-gradient(90deg, var(--color-primary-dark), var(--color-primary));
|
||||
color: var(--color-light);
|
||||
padding: 15px 20px; /* Mais espaçamento para uma navegação mais confortável */
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-logo h2 {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: var(--color-light);
|
||||
}
|
||||
|
||||
.navbar-links {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.navbar-links li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.navbar-links a {
|
||||
color: var(--color-light);
|
||||
text-decoration: none;
|
||||
font-size: 18px;
|
||||
padding: 8px 15px;
|
||||
border-radius: var(--border-radius);
|
||||
transition: background-color var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.navbar-links a:hover {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-light);
|
||||
}
|
||||
|
||||
.navbar-links a.active {
|
||||
background-color: var(--color-primary-dark);
|
||||
color: var(--color-light);
|
||||
}
|
||||
|
||||
/* Ícone do menu hamburguer para telas pequenas */
|
||||
.menu-icon {
|
||||
display: none;
|
||||
font-size: 30px;
|
||||
color: white;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Responsividade para telas pequenas */
|
||||
@media (max-width: 768px) {
|
||||
.navbar-links {
|
||||
display: none; /* Inicialmente oculta os links */
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.navbar-links.active {
|
||||
display: flex; /* Exibe os links quando o menu estiver ativo */
|
||||
}
|
||||
|
||||
.navbar-links li {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.navbar-links a {
|
||||
padding: 10px 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Exibe o ícone do menu hamburguer */
|
||||
.menu-icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Estilos para a Página de Segurança */
|
||||
.security-container {
|
||||
background-color: var(--color-light);
|
||||
padding: 20px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.security-title {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
.security-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.security-item label {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.security-item input[type="checkbox"] {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.security-item ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.security-item ul li {
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.security-item select {
|
||||
margin-left: 10px;
|
||||
padding: 5px;
|
||||
font-size: 16px;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
|
||||
/* Estilos para a Página de Segurança */
|
||||
.security-container {
|
||||
background-color: var(--color-light);
|
||||
padding: 20px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.security-title {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
.security-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.security-item label {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.security-item input[type="checkbox"] {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.auth-methods label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.security-item select {
|
||||
margin-left: 10px;
|
||||
padding: 5px;
|
||||
font-size: 16px;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.add-user button {
|
||||
background-color: var(--color-info); /* Cor azul para adicionar */
|
||||
}
|
||||
|
||||
.add-user button:hover {
|
||||
background-color: var(--color-info-dark);
|
||||
}
|
||||
|
||||
/* Estilos para a lista de usuários */
|
||||
.security-item ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.security-item ul li {
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.security-item button {
|
||||
background-color: var(--color-alert); /* Cor vermelha para remover */
|
||||
padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius);
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.security-item button:hover {
|
||||
background-color: var(--color-alert-dark);
|
||||
}
|
||||
|
||||
|
||||
/* Estilos para o Dashboard */
|
||||
.dashboard-container {
|
||||
background-color: var(--color-light);
|
||||
padding: 20px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
.dashboard-summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--color-surface);
|
||||
padding: 20px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
width: 30%;
|
||||
text-align: center;
|
||||
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
font-size: 20px;
|
||||
color: var(--color-dark);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card p {
|
||||
font-size: 18px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.alerts {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.alerts h2 {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
font-size: 16px;
|
||||
background-color: var(--color-alert);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.chargers-table {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.chargers-table h2 {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--color-table-header);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-summary {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Layout comum para páginas */
|
||||
.page-container {
|
||||
max-width: 900px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
background-color: var(--color-surface);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Formulários */
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
width: 100%;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Alinhamento de texto e checkbox */
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--color-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.button-grid {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Mensagens de feedback */
|
||||
.message {
|
||||
padding: 10px 15px;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 15px;
|
||||
background-color: var(--color-table-header);
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-light);
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background-color: var(--color-alert);
|
||||
color: var(--color-light);
|
||||
}
|
||||
|
||||
/* Caixa de log */
|
||||
.log-box {
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 15px;
|
||||
border-radius: var(--border-radius);
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css'; // Importe o CSS unificado aqui
|
||||
import './index.css'; // ou 'styles.css'
|
||||
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
|
||||
@@ -1,66 +1,64 @@
|
||||
// src/pages/Connectivity.jsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { get, post } from '../api';
|
||||
import PageLayout from '../components/PageLayout';
|
||||
import Alert from '../components/Alert';
|
||||
|
||||
const Connectivity = () => {
|
||||
const [status, setStatus] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [wifiConfig, setWifiConfig] = useState({ ssid: '', password: '' });
|
||||
const [wifiNetworks, setWifiNetworks] = useState([]);
|
||||
const [wifiConfig, setWifiConfig] = useState({ enabled: false, ssid: '', password: '' });
|
||||
const [wifiMsg, setWifiMsg] = useState('');
|
||||
|
||||
const [mqttConfig, setMqttConfig] = useState({
|
||||
enabled: false,
|
||||
host: '',
|
||||
port: 1883,
|
||||
port: 1883, // Inicialmente trata-se como número
|
||||
username: '',
|
||||
password: '',
|
||||
topic: '',
|
||||
});
|
||||
const [mqttMsg, setMqttMsg] = useState('');
|
||||
|
||||
// Carregar as configurações Wi-Fi e MQTT
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const conn = await get('/api/v1/connectivity');
|
||||
setStatus(conn);
|
||||
} catch {
|
||||
// ignore errors in demo
|
||||
}
|
||||
try {
|
||||
const wifi = await get('/api/v1/config/wifi');
|
||||
setWifiConfig(wifi);
|
||||
} catch {}
|
||||
try {
|
||||
const list = await get('/api/v1/config/wifi/scan');
|
||||
setWifiNetworks(list.networks || []);
|
||||
} catch {}
|
||||
setWifiConfig(wifi); // Atualiza as configurações Wi-Fi
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar configurações Wi-Fi:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
const mqtt = await get('/api/v1/config/mqtt');
|
||||
setMqttConfig(mqtt);
|
||||
} catch {}
|
||||
setLoading(false);
|
||||
setMqttConfig(mqtt); // Atualiza as configurações MQTT
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar configurações MQTT:', error);
|
||||
}
|
||||
|
||||
setLoading(false); // Finaliza o carregamento
|
||||
};
|
||||
|
||||
load();
|
||||
}, []);
|
||||
|
||||
// Salvar configuração Wi-Fi
|
||||
const saveWifi = async () => {
|
||||
try {
|
||||
await post('/api/v1/config/wifi', wifiConfig);
|
||||
setWifiMsg('Configuração Wi-Fi gravada!');
|
||||
} catch {
|
||||
setWifiMsg('Erro ao gravar Wi-Fi.');
|
||||
setWifiMsg('Alterações guardadas com sucesso!');
|
||||
} catch (error) {
|
||||
setWifiMsg('Erro ao guardar alterações.');
|
||||
}
|
||||
};
|
||||
|
||||
// Salvar configuração MQTT
|
||||
const saveMqtt = async () => {
|
||||
try {
|
||||
await post('/api/v1/config/mqtt', mqttConfig);
|
||||
setMqttMsg('Configuração MQTT gravada!');
|
||||
} catch {
|
||||
setMqttMsg('Erro ao gravar MQTT.');
|
||||
setMqttMsg('Alterações guardadas com sucesso!');
|
||||
} catch (error) {
|
||||
setMqttMsg('Erro ao guardar alterações.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,49 +68,61 @@ const Connectivity = () => {
|
||||
<p>A carregar...</p>
|
||||
) : (
|
||||
<>
|
||||
{status && (
|
||||
{/* Configuração Wi-Fi */}
|
||||
<h2 className="text-xl font-semibold mt-4">Configuração Wi-Fi</h2>
|
||||
{wifiMsg && <Alert type={wifiMsg.startsWith('Erro') ? 'error' : 'success'}>{wifiMsg}</Alert>}
|
||||
<form className="flex flex-col gap-4" onSubmit={e => { e.preventDefault(); saveWifi(); }}>
|
||||
<div>
|
||||
<h2>Status Atual</h2>
|
||||
<p>Wi-Fi: {status.wifi.status} ({status.wifi.ssid})</p>
|
||||
<p>MQTT: {status.mqtt.status} - {status.mqtt.broker}:{status.mqtt.port}</p>
|
||||
<label className="flex items-center gap-2">
|
||||
Ativar WIFI
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={wifiConfig.enabled}
|
||||
onChange={e => setWifiConfig({ ...wifiConfig, enabled: e.target.checked })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h2>Configuração Wi-Fi</h2>
|
||||
{wifiMsg && <div className="message">{wifiMsg}</div>}
|
||||
<form className="form" onSubmit={e => { e.preventDefault(); saveWifi(); }}>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="wifi-ssid">SSID:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="wifi-ssid">SSID:</label>
|
||||
<input
|
||||
id="wifi-ssid"
|
||||
type="text"
|
||||
className={`border border-gray-300 rounded px-3 py-2 w-full ${!wifiConfig.enabled ? 'bg-gray-200 text-gray-500 cursor-not-allowed' : ''}`}
|
||||
value={wifiConfig.ssid}
|
||||
onChange={e => setWifiConfig({ ...wifiConfig, ssid: e.target.value })}
|
||||
disabled={!wifiConfig.enabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="wifi-password">Palavra-passe:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="wifi-password">Palavra-passe:</label>
|
||||
<input
|
||||
id="wifi-password"
|
||||
type="password"
|
||||
className={`border border-gray-300 rounded px-3 py-2 w-full ${!wifiConfig.enabled ? 'bg-gray-200 text-gray-500 cursor-not-allowed' : ''}`}
|
||||
value={wifiConfig.password}
|
||||
onChange={e => setWifiConfig({ ...wifiConfig, password: e.target.value })}
|
||||
disabled={!wifiConfig.enabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="button-grid">
|
||||
<button type="submit">Guardar</button>
|
||||
<div>
|
||||
<button
|
||||
className={`bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700`}
|
||||
type="submit"
|
||||
>
|
||||
Guardar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h2>Configuração MQTT</h2>
|
||||
{mqttMsg && <div className="message">{mqttMsg}</div>}
|
||||
<form className="form" onSubmit={e => { e.preventDefault(); saveMqtt(); }}>
|
||||
<div className="form-group">
|
||||
<label className="checkbox-label">
|
||||
{/* Configuração MQTT */}
|
||||
<h2 className="text-xl font-semibold mt-6">Configuração MQTT</h2>
|
||||
{mqttMsg && <Alert type={mqttMsg.startsWith('Erro') ? 'error' : 'success'}>{mqttMsg}</Alert>}
|
||||
<form className="flex flex-col gap-4" onSubmit={e => { e.preventDefault(); saveMqtt(); }}>
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
Ativar MQTT
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -122,58 +132,73 @@ const Connectivity = () => {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="mqtt-host">Host:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="mqtt-host">Host:</label>
|
||||
<input
|
||||
id="mqtt-host"
|
||||
type="text"
|
||||
className={`border border-gray-300 rounded px-3 py-2 w-full ${!mqttConfig.enabled ? 'bg-gray-200 text-gray-500 cursor-not-allowed' : ''}`}
|
||||
value={mqttConfig.host}
|
||||
onChange={e => setMqttConfig({ ...mqttConfig, host: e.target.value })}
|
||||
disabled={!mqttConfig.enabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="mqtt-port">Porta:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="mqtt-port">Porta:</label>
|
||||
<input
|
||||
id="mqtt-port"
|
||||
type="number"
|
||||
className={`border border-gray-300 rounded px-3 py-2 w-full ${!mqttConfig.enabled ? 'bg-gray-200 text-gray-500 cursor-not-allowed' : ''}`}
|
||||
value={mqttConfig.port}
|
||||
onChange={e => setMqttConfig({ ...mqttConfig, port: parseInt(e.target.value || 0) })}
|
||||
onChange={e => setMqttConfig({ ...mqttConfig, port: parseInt(e.target.value || 0, 10) || 1883 })}
|
||||
disabled={!mqttConfig.enabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="mqtt-username">Utilizador:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="mqtt-username">Utilizador:</label>
|
||||
<input
|
||||
id="mqtt-username"
|
||||
type="text"
|
||||
className={`border border-gray-300 rounded px-3 py-2 w-full ${!mqttConfig.enabled ? 'bg-gray-200 text-gray-500 cursor-not-allowed' : ''}`}
|
||||
value={mqttConfig.username}
|
||||
onChange={e => setMqttConfig({ ...mqttConfig, username: e.target.value })}
|
||||
disabled={!mqttConfig.enabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="mqtt-password">Palavra-passe:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="mqtt-password">Palavra-passe:</label>
|
||||
<input
|
||||
id="mqtt-password"
|
||||
type="password"
|
||||
className={`border border-gray-300 rounded px-3 py-2 w-full ${!mqttConfig.enabled ? 'bg-gray-200 text-gray-500 cursor-not-allowed' : ''}`}
|
||||
value={mqttConfig.password}
|
||||
onChange={e => setMqttConfig({ ...mqttConfig, password: e.target.value })}
|
||||
disabled={!mqttConfig.enabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="mqtt-topic">Tópico:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="mqtt-topic">Tópico:</label>
|
||||
<input
|
||||
id="mqtt-topic"
|
||||
type="text"
|
||||
className={`border border-gray-300 rounded px-3 py-2 w-full ${!mqttConfig.enabled ? 'bg-gray-200 text-gray-500 cursor-not-allowed' : ''}`}
|
||||
value={mqttConfig.topic}
|
||||
onChange={e => setMqttConfig({ ...mqttConfig, topic: e.target.value })}
|
||||
disabled={!mqttConfig.enabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="button-grid">
|
||||
<button type="submit">Guardar</button>
|
||||
<div>
|
||||
<button
|
||||
className={`bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700`}
|
||||
type="submit"
|
||||
>
|
||||
Guardar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
|
||||
@@ -1,46 +1,67 @@
|
||||
// src/pages/Dashboard.jsx
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PageLayout from '../components/PageLayout';
|
||||
import Alert from '../components/Alert';
|
||||
|
||||
const Dashboard = () => {
|
||||
// Mock data (substitua pelos dados reais)
|
||||
const mockDashboardData = {
|
||||
// Estados para armazenar os dados do dashboard
|
||||
const [dashboardData, setDashboardData] = useState({
|
||||
status: "Ativo",
|
||||
chargers: [
|
||||
{ id: 1, status: "Ativo", current: 12, power: 2200 },
|
||||
{ id: 2, status: "Inativo", current: 0, power: 0 },
|
||||
{ id: 3, status: "Erro", current: 0, power: 0 },
|
||||
],
|
||||
energyConsumed: 50.3,
|
||||
chargingTime: 120,
|
||||
alerts: ["Aviso: Carregador 1 está com erro."],
|
||||
energyConsumed: 0,
|
||||
chargingTime: 0,
|
||||
alerts: [],
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Função para obter os dados do dashboard
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/dashboard');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setDashboardData(data); // Atualiza o estado com os dados recebidos
|
||||
} else {
|
||||
setError('Erro ao obter os dados do dashboard');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar dados do dashboard:', error);
|
||||
setError('Erro de conexão');
|
||||
}
|
||||
};
|
||||
|
||||
// Chamar a função fetchDashboardData quando o componente for montado
|
||||
useEffect(() => {
|
||||
fetchDashboardData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="dashboard-container">
|
||||
<h1 className="dashboard-title">Visão Geral</h1>
|
||||
<PageLayout title="Visão Geral">
|
||||
{error && <Alert type="error">{error}</Alert>}
|
||||
|
||||
{/* Cards com informações resumidas */}
|
||||
<div className="dashboard-summary">
|
||||
<div className="card">
|
||||
<div className="flex flex-wrap gap-4 mb-6">
|
||||
<div className="bg-white p-4 rounded shadow flex-1 min-w-[150px]">
|
||||
<h3>Status do Sistema</h3>
|
||||
<p>{mockDashboardData.status}</p>
|
||||
<p>{dashboardData.status}</p>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="bg-white p-4 rounded shadow flex-1 min-w-[150px]">
|
||||
<h3>Consumo de Energia</h3>
|
||||
<p>{mockDashboardData.energyConsumed} kWh</p>
|
||||
<p>{dashboardData.energyConsumed} kWh</p>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="bg-white p-4 rounded shadow flex-1 min-w-[150px]">
|
||||
<h3>Tempo de Carregamento</h3>
|
||||
<p>{mockDashboardData.chargingTime} minutos</p>
|
||||
<p>{dashboardData.chargingTime} minutos</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Indicadores de falhas ou alertas */}
|
||||
<div className="alerts">
|
||||
<h2>Alertas</h2>
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold mb-2">Alertas</h2>
|
||||
<ul>
|
||||
{mockDashboardData.alerts.map((alert, index) => (
|
||||
<li key={index} className="alert-item">
|
||||
{dashboardData.alerts.map((alert, index) => (
|
||||
<li key={index} className="p-2 bg-red-500 text-white rounded mb-2">
|
||||
<span>⚠️ {alert}</span>
|
||||
</li>
|
||||
))}
|
||||
@@ -48,30 +69,30 @@ const Dashboard = () => {
|
||||
</div>
|
||||
|
||||
{/* Tabela de Carregadores */}
|
||||
<div className="chargers-table">
|
||||
<h2>Carregadores</h2>
|
||||
<table>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-2">Carregadores</h2>
|
||||
<table className="min-w-full border border-gray-300 text-left">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Status</th>
|
||||
<th>Corrente (A)</th>
|
||||
<th>Potência (W)</th>
|
||||
<th className="border-b p-2">ID</th>
|
||||
<th className="border-b p-2">Status</th>
|
||||
<th className="border-b p-2">Corrente (A)</th>
|
||||
<th className="border-b p-2">Potência (W)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{mockDashboardData.chargers.map((charger) => (
|
||||
{dashboardData.chargers.map((charger) => (
|
||||
<tr key={charger.id}>
|
||||
<td>{charger.id}</td>
|
||||
<td>{charger.status}</td>
|
||||
<td>{charger.current}</td>
|
||||
<td>{charger.power}</td>
|
||||
<td className="border-b p-2">{charger.id}</td>
|
||||
<td className="border-b p-2">{charger.status}</td>
|
||||
<td className="border-b p-2">{charger.current}</td>
|
||||
<td className="border-b p-2">{charger.power}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { get, post } from '../api';
|
||||
import PageLayout from '../components/PageLayout';
|
||||
import Alert from '../components/Alert';
|
||||
|
||||
export default function ElectricalNetwork() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [msg, setMsg] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const [monitor, setMonitor] = useState({ voltage: '', current: '', quality: '' });
|
||||
const [monitor, setMonitor] = useState({ voltage: '0', current: '0', quality: '0' });
|
||||
const [alerts, setAlerts] = useState(false);
|
||||
const [security, setSecurity] = useState({ earthFault: false, rcm: false });
|
||||
const [loadBalancing, setLoadBalancing] = useState({ enabled: false, currentLimit: 32 });
|
||||
const [loadBalancing, setLoadBalancing] = useState({ enabled: false, currentLimit: 0 });
|
||||
const [solar, setSolar] = useState({ capacity: 0, useSolar: false, handleExcess: false });
|
||||
|
||||
// Função para carregar as configurações do backend
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
@@ -22,8 +24,9 @@ export default function ElectricalNetwork() {
|
||||
if (cfg.security) setSecurity(cfg.security);
|
||||
if (cfg.loadBalancing) setLoadBalancing(cfg.loadBalancing);
|
||||
if (cfg.solar) setSolar(cfg.solar);
|
||||
} catch {
|
||||
// endpoint opcional
|
||||
} catch (err) {
|
||||
console.error('Erro ao carregar configurações:', err);
|
||||
// Endpoint opcional, sem ações adicionais
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -31,54 +34,78 @@ export default function ElectricalNetwork() {
|
||||
load();
|
||||
}, []);
|
||||
|
||||
// Função para salvar as configurações no backend
|
||||
const save = async () => {
|
||||
setMsg('');
|
||||
setError('');
|
||||
try {
|
||||
const body = { monitor, alerts, security, loadBalancing, solar };
|
||||
await post('/api/v1/config/electrical', body);
|
||||
setMsg('Configuração gravada com sucesso!');
|
||||
} catch {
|
||||
setError('Erro ao gravar configuração.');
|
||||
setMsg('Alterações guardadas com sucesso!');
|
||||
} catch (err) {
|
||||
console.error('Erro ao salvar configuração:', err);
|
||||
setError('Erro ao guardar alterações.');
|
||||
}
|
||||
};
|
||||
|
||||
// Função para lidar com as alterações de valores numéricos
|
||||
const handleLoadBalancingCurrentLimitChange = (e) => {
|
||||
setLoadBalancing({
|
||||
...loadBalancing,
|
||||
currentLimit: parseInt(e.target.value, 10) || 0 // Converte para número
|
||||
});
|
||||
};
|
||||
|
||||
const handleSolarCapacityChange = (e) => {
|
||||
setSolar({
|
||||
...solar,
|
||||
capacity: parseFloat(e.target.value) || 0 // Converte para número
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout title="Rede Elétrica">
|
||||
{msg && <div className="message success">{msg}</div>}
|
||||
{error && <div className="message error">{error}</div>}
|
||||
{msg && <Alert type="success">{msg}</Alert>}
|
||||
{error && <Alert type="error">{error}</Alert>}
|
||||
|
||||
{loading ? (
|
||||
<p>A carregar...</p>
|
||||
) : (
|
||||
<form className="form" onSubmit={e => { e.preventDefault(); save(); }}>
|
||||
<h2>Monitoramento da Rede Elétrica</h2>
|
||||
<div className="form-group">
|
||||
<label>Tensão de Entrada (V):</label>
|
||||
<form className="flex flex-col gap-4" onSubmit={e => { e.preventDefault(); save(); }}>
|
||||
<h2 className="text-xl font-semibold">Monitoramento da Rede Elétrica</h2>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1">Tensão de Entrada (V):</label>
|
||||
<input
|
||||
type="number"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={monitor.voltage}
|
||||
onChange={e => setMonitor({ ...monitor, voltage: e.target.value })}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Corrente de Entrada (A):</label>
|
||||
<input
|
||||
type="number"
|
||||
value={monitor.current}
|
||||
onChange={e => setMonitor({ ...monitor, current: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Qualidade de Energia:</label>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1">Corrente de Entrada (A):</label>
|
||||
<input
|
||||
type="text"
|
||||
value={monitor.quality}
|
||||
onChange={e => setMonitor({ ...monitor, quality: e.target.value })}
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={monitor.current}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="checkbox-label">
|
||||
|
||||
<div>
|
||||
<label className="block mb-1">Qualidade de Energia:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={monitor.quality}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
Alertas de Falha na Rede
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -88,9 +115,9 @@ export default function ElectricalNetwork() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h2>Proteção de Segurança Elétrica</h2>
|
||||
<div className="form-group">
|
||||
<label className="checkbox-label">
|
||||
<h2 className="text-xl font-semibold">Proteção de Segurança Elétrica</h2>
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
Detecção de Falha de Aterramento
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -99,8 +126,8 @@ export default function ElectricalNetwork() {
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="checkbox-label">
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
Proteção RCM
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -110,9 +137,9 @@ export default function ElectricalNetwork() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h2>Balanceamento de Carga</h2>
|
||||
<div className="form-group">
|
||||
<label className="checkbox-label">
|
||||
<h2 className="text-xl font-semibold">Balanceamento de Carga</h2>
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
Habilitar Balanceamento de Carga
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -121,28 +148,31 @@ export default function ElectricalNetwork() {
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="lb-current">Limite de Corrente (A):</label>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="lb-current">Limite de Corrente (A):</label>
|
||||
<input
|
||||
id="lb-current"
|
||||
type="number"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={loadBalancing.currentLimit}
|
||||
onChange={e => setLoadBalancing({ ...loadBalancing, currentLimit: e.target.value })}
|
||||
onChange={handleLoadBalancingCurrentLimitChange} // Aplicando a conversão para número
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h2>Energia Solar</h2>
|
||||
<div className="form-group">
|
||||
<label htmlFor="solar-capacity">Capacidade Solar (kW):</label>
|
||||
<h2 className="text-xl font-semibold">Energia Solar</h2>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="solar-capacity">Capacidade Solar (kW):</label>
|
||||
<input
|
||||
id="solar-capacity"
|
||||
type="number"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={solar.capacity}
|
||||
onChange={e => setSolar({ ...solar, capacity: e.target.value })}
|
||||
onChange={handleSolarCapacityChange} // Aplicando a conversão para número
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="checkbox-label">
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
Direcionar Energia Solar para o EVSE
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -151,8 +181,8 @@ export default function ElectricalNetwork() {
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="checkbox-label">
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
Gerenciamento de Excesso de Energia Solar
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -162,12 +192,13 @@ export default function ElectricalNetwork() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="button-grid">
|
||||
<button type="submit">Guardar</button>
|
||||
<div>
|
||||
<button className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700" type="submit">
|
||||
Guardar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// src/pages/LoadBalancing.js
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { get, post } from '../api';
|
||||
import PageLayout from '../components/PageLayout';
|
||||
import Alert from '../components/Alert';
|
||||
|
||||
export default function LoadBalancing() {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
@@ -37,7 +36,7 @@ export default function LoadBalancing() {
|
||||
devices: selectedDevices,
|
||||
};
|
||||
await post('/api/v1/config/load-balancing', config);
|
||||
setMsg('Configuração de Load Balancing salva com sucesso!');
|
||||
setMsg('Alterações guardadas com sucesso!');
|
||||
} catch {
|
||||
setError('Erro ao salvar configuração de Load Balancing.');
|
||||
}
|
||||
@@ -66,15 +65,20 @@ export default function LoadBalancing() {
|
||||
}
|
||||
};
|
||||
|
||||
// Função para lidar com a mudança na corrente máxima
|
||||
const handleMaxChargingCurrentChange = (e) => {
|
||||
setMaxChargingCurrent(parseInt(e.target.value, 10)); // Garantir que o valor seja um número
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout title="Configuração de Load Balancing">
|
||||
{msg && <div className="message success">{msg}</div>}
|
||||
{error && <div className="message error">{error}</div>}
|
||||
{msg && <Alert type="success">{msg}</Alert>}
|
||||
{error && <Alert type="error">{error}</Alert>}
|
||||
|
||||
<form className="form" onSubmit={saveConfig}>
|
||||
<form className="flex flex-col gap-4" onSubmit={saveConfig}>
|
||||
{/* Controle de Ativação/Desativação */}
|
||||
<div className="form-group">
|
||||
<label className="checkbox-label">
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
Ativar Load Balancing
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -85,8 +89,8 @@ export default function LoadBalancing() {
|
||||
</div>
|
||||
|
||||
{/* Configuração de Corrente Máxima */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="maxChargingCurrent">
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="maxChargingCurrent">
|
||||
Corrente Máxima para Balanceamento (A):
|
||||
</label>
|
||||
<input
|
||||
@@ -95,16 +99,17 @@ export default function LoadBalancing() {
|
||||
value={maxChargingCurrent}
|
||||
min="1"
|
||||
max="32"
|
||||
onChange={(e) => setMaxChargingCurrent(e.target.value)}
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
onChange={handleMaxChargingCurrentChange} // Alterado para usar a função que converte o valor
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Seleção de Dispositivos */}
|
||||
<div className="form-group">
|
||||
<label>Dispositivos a Balancear:</label>
|
||||
<div>
|
||||
<label className="block mb-1">Dispositivos a Balancear:</label>
|
||||
{devices.map((device) => (
|
||||
<div key={device.id}>
|
||||
<label className="checkbox-label">
|
||||
<label className="flex items-center gap-2">
|
||||
{device.name}
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -117,8 +122,8 @@ export default function LoadBalancing() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="button-grid">
|
||||
<button type="submit">Salvar Configuração</button>
|
||||
<div>
|
||||
<button className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700" type="submit">Salvar Configuração</button>
|
||||
</div>
|
||||
</form>
|
||||
</PageLayout>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import PageLayout from '../components/PageLayout';
|
||||
import Alert from '../components/Alert';
|
||||
|
||||
export default function Login({ setAuthData }) {
|
||||
const [user, setUser] = useState('');
|
||||
@@ -18,31 +19,33 @@ export default function Login({ setAuthData }) {
|
||||
|
||||
return (
|
||||
<PageLayout title="Início de Sessão">
|
||||
{error && <div className="message error">{error}</div>}
|
||||
{error && <Alert type="error">{error}</Alert>}
|
||||
|
||||
<form className="form" onSubmit={submit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="user">Utilizador:</label>
|
||||
<form className="flex flex-col gap-4" onSubmit={submit}>
|
||||
<div className="mb-4">
|
||||
<label className="block mb-1" htmlFor="user">Utilizador:</label>
|
||||
<input
|
||||
id="user"
|
||||
type="text"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={user}
|
||||
onChange={e => setUser(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="pass">Palavra-passe:</label>
|
||||
<div className="mb-4">
|
||||
<label className="block mb-1" htmlFor="pass">Palavra-passe:</label>
|
||||
<input
|
||||
id="pass"
|
||||
type="password"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={pass}
|
||||
onChange={e => setPass(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="button-grid">
|
||||
<button type="submit">Entrar</button>
|
||||
<div className="mt-4">
|
||||
<button className="bg-green-600 text-white px-4 py-2 rounded w-full hover:bg-green-700" type="submit">Entrar</button>
|
||||
</div>
|
||||
</form>
|
||||
</PageLayout>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { fetchLogs } from '../api';
|
||||
import PageLayout from '../components/PageLayout';
|
||||
import Alert from '../components/Alert';
|
||||
|
||||
export default function Logs() {
|
||||
const [logs, setLogs] = useState('');
|
||||
@@ -26,12 +27,12 @@ export default function Logs() {
|
||||
|
||||
return (
|
||||
<PageLayout title="Registos do Sistema">
|
||||
{error && <div className="message error">{error}</div>}
|
||||
{error && <Alert type="error">{error}</Alert>}
|
||||
|
||||
{loading ? (
|
||||
<p>A carregar...</p>
|
||||
) : (
|
||||
<pre className="log-box">{logs || 'Sem dados.'}</pre>
|
||||
<pre className="bg-white border border-gray-300 p-3 rounded max-h-96 overflow-auto">{logs || 'Sem dados.'}</pre>
|
||||
)}
|
||||
</PageLayout>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { get, post } from '../api';
|
||||
import PageLayout from '../components/PageLayout';
|
||||
import Alert from '../components/Alert';
|
||||
|
||||
export default function Mqtt() {
|
||||
const [config, setConfig] = useState({
|
||||
@@ -28,23 +29,23 @@ export default function Mqtt() {
|
||||
setError('');
|
||||
try {
|
||||
await post('/api/v1/config/mqtt', config);
|
||||
setMsg('Configuração gravada com sucesso!');
|
||||
setMsg('Alterações guardadas com sucesso!');
|
||||
} catch {
|
||||
setError('Erro ao gravar configuração.');
|
||||
setError('Erro ao guardar alterações.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout title="Configuração MQTT">
|
||||
{msg && <div className="message success">{msg}</div>}
|
||||
{error && <div className="message error">{error}</div>}
|
||||
{msg && <Alert type="success">{msg}</Alert>}
|
||||
{error && <Alert type="error">{error}</Alert>}
|
||||
|
||||
{loading ? (
|
||||
<p>A carregar...</p>
|
||||
) : (
|
||||
<form className="form" onSubmit={e => { e.preventDefault(); save(); }}>
|
||||
<div className="form-group">
|
||||
<label className="checkbox-label">
|
||||
<form className="flex flex-col gap-4" onSubmit={e => { e.preventDefault(); save(); }}>
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
Ativar MQTT
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -54,58 +55,63 @@ export default function Mqtt() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="host">Host:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="host">Host:</label>
|
||||
<input
|
||||
id="host"
|
||||
type="text"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={config.host}
|
||||
onChange={e => setConfig({ ...config, host: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="port">Porta:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="port">Porta:</label>
|
||||
<input
|
||||
id="port"
|
||||
type="number"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={config.port}
|
||||
onChange={e => setConfig({ ...config, port: parseInt(e.target.value || 0) })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="username">Utilizador:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="username">Utilizador:</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={config.username}
|
||||
onChange={e => setConfig({ ...config, username: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Palavra-passe:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="password">Palavra-passe:</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={config.password}
|
||||
onChange={e => setConfig({ ...config, password: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="topic">Tópico:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="topic">Tópico:</label>
|
||||
<input
|
||||
id="topic"
|
||||
type="text"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={config.topic}
|
||||
onChange={e => setConfig({ ...config, topic: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="button-grid">
|
||||
<button type="submit">Guardar</button>
|
||||
<div>
|
||||
<button className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700" type="submit">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { get, post } from '../api';
|
||||
import PageLayout from '../components/PageLayout';
|
||||
import Alert from '../components/Alert';
|
||||
|
||||
const OCPP = () => {
|
||||
const [status, setStatus] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [config, setConfig] = useState({
|
||||
enabled: false,
|
||||
url: '',
|
||||
chargeBoxId: '',
|
||||
certificate: '',
|
||||
@@ -36,9 +38,9 @@ const OCPP = () => {
|
||||
setMsg('');
|
||||
try {
|
||||
await post('/api/v1/config/ocpp', config);
|
||||
setMsg('Configuração gravada com sucesso!');
|
||||
setMsg('Alterações guardadas com sucesso!');
|
||||
} catch {
|
||||
setMsg('Erro ao gravar configuração.');
|
||||
setMsg('Erro ao guardar alterações.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -48,57 +50,76 @@ const OCPP = () => {
|
||||
<p>A carregar...</p>
|
||||
) : (
|
||||
<>
|
||||
{status && (
|
||||
<div>
|
||||
<p>Versão: {status.ocpp_version}</p>
|
||||
<p>Status: {status.status}</p>
|
||||
</div>
|
||||
{msg && (
|
||||
<Alert type={msg.startsWith('Erro') ? 'error' : 'success'}>{msg}</Alert>
|
||||
)}
|
||||
|
||||
{msg && <div className="message">{msg}</div>}
|
||||
<form className="form" onSubmit={e => { e.preventDefault(); save(); }}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="ocpp-url">Servidor:</label>
|
||||
|
||||
<form className="flex flex-col gap-4" onSubmit={e => { e.preventDefault(); save(); }}>
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
Ativar OCPP
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enabled}
|
||||
onChange={e => setConfig({ ...config, enabled: e.target.checked })} // Corrigido para usar e.target.checked
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="ocpp-url">Servidor:</label>
|
||||
<input
|
||||
id="ocpp-url"
|
||||
type="text"
|
||||
className={`border border-gray-300 rounded px-3 py-2 w-full ${!config.enabled ? 'bg-gray-200 text-gray-500 cursor-not-allowed' : ''}`}
|
||||
value={config.url}
|
||||
onChange={e => setConfig({ ...config, url: e.target.value })}
|
||||
disabled={!config.enabled} // Desabilita o campo se o checkbox estiver desmarcado
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="ocpp-id">Charge Box ID:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="ocpp-id">Charge Box ID:</label>
|
||||
<input
|
||||
id="ocpp-id"
|
||||
type="text"
|
||||
className={`border border-gray-300 rounded px-3 py-2 w-full ${!config.enabled ? 'bg-gray-200 text-gray-500 cursor-not-allowed' : ''}`}
|
||||
value={config.chargeBoxId}
|
||||
onChange={e => setConfig({ ...config, chargeBoxId: e.target.value })}
|
||||
disabled={!config.enabled} // Desabilita o campo se o checkbox estiver desmarcado
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="ocpp-cert">Certificado:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="ocpp-cert">Certificado:</label>
|
||||
<textarea
|
||||
id="ocpp-cert"
|
||||
className={`border border-gray-300 rounded px-3 py-2 w-full ${!config.enabled ? 'bg-gray-200 text-gray-500 cursor-not-allowed' : ''}`}
|
||||
value={config.certificate}
|
||||
onChange={e => setConfig({ ...config, certificate: e.target.value })}
|
||||
disabled={!config.enabled} // Desabilita o campo se o checkbox estiver desmarcado
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="ocpp-key">Chave Privada:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="ocpp-key">Chave Privada:</label>
|
||||
<textarea
|
||||
id="ocpp-key"
|
||||
className={`border border-gray-300 rounded px-3 py-2 w-full ${!config.enabled ? 'bg-gray-200 text-gray-500 cursor-not-allowed' : ''}`}
|
||||
value={config.privateKey}
|
||||
onChange={e => setConfig({ ...config, privateKey: e.target.value })}
|
||||
disabled={!config.enabled} // Desabilita o campo se o checkbox estiver desmarcado
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="button-grid">
|
||||
<button type="submit">Guardar</button>
|
||||
<div>
|
||||
<button className={`bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700`} type="submit">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
</>
|
||||
)}
|
||||
</PageLayout>
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
// src/pages/Security.jsx
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PageLayout from '../components/PageLayout';
|
||||
import Alert from '../components/Alert';
|
||||
import { post, get } from '../api'; // Supondo que os métodos post e get estejam definidos na API.
|
||||
|
||||
const Security = () => {
|
||||
// Estado para armazenar se MFA está habilitado e os métodos de autenticação
|
||||
const [isMFAEnabled, setIsMFAEnabled] = useState(false);
|
||||
// Estado para armazenar os métodos de autenticação
|
||||
const [authMethods, setAuthMethods] = useState({
|
||||
RFID: false,
|
||||
App: false,
|
||||
Password: true,
|
||||
Password: false,
|
||||
});
|
||||
|
||||
// Estado para armazenar a lista de usuários
|
||||
const [users, setUsers] = useState([
|
||||
{ username: 'admin' },
|
||||
{ username: 'user1' },
|
||||
]);
|
||||
|
||||
// Estado para o novo nome de usuário
|
||||
const [newUser, setNewUser] = useState('');
|
||||
const [msg, setMsg] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Função para alterar os métodos de autenticação
|
||||
const handleAuthMethodChange = (method) => {
|
||||
setAuthMethods({
|
||||
...authMethods,
|
||||
@@ -21,24 +29,57 @@ const Security = () => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const addUser = (username) => {
|
||||
setUsers([...users, { username }]);
|
||||
// Função para buscar os dados de autenticação da API
|
||||
const fetchAuthMethods = async () => {
|
||||
try {
|
||||
const data = await get('/api/v1/config/auth-methods'); // Busca os dados de authMethods da API
|
||||
setAuthMethods(data); // Preenche o estado com os dados recebidos
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar configurações de autenticação:', error);
|
||||
setError('Erro ao buscar configurações de autenticação.');
|
||||
}
|
||||
};
|
||||
|
||||
// Função para enviar os dados para a API
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault(); // Evita que a página seja recarregada ao enviar o formulário
|
||||
|
||||
try {
|
||||
await post('/api/v1/config/auth-methods', authMethods); // Envia os dados de authMethods para o servidor
|
||||
setMsg('Configurações de Autorização salvas com sucesso!');
|
||||
} catch (error) {
|
||||
console.error('Erro ao salvar configurações:', error);
|
||||
setError('Erro ao salvar configurações.');
|
||||
}
|
||||
};
|
||||
|
||||
// Função para adicionar um novo usuário
|
||||
const addUser = () => {
|
||||
if (newUser.trim() !== '') {
|
||||
setUsers([...users, { username: newUser }]);
|
||||
setNewUser(''); // Limpa o campo de entrada após adicionar
|
||||
}
|
||||
};
|
||||
|
||||
// Função para remover um usuário
|
||||
const removeUser = (username) => {
|
||||
setUsers(users.filter((user) => user.username !== username));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="security-container">
|
||||
<h1 className="security-title">Segurança</h1>
|
||||
// Use o useEffect para buscar os dados de autenticação quando o componente for montado
|
||||
useEffect(() => {
|
||||
fetchAuthMethods();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageLayout title="Segurança">
|
||||
{msg && <Alert type="success">{msg}</Alert>}
|
||||
{error && <Alert type="error">{error}</Alert>}
|
||||
{/* Métodos de Autorização */}
|
||||
<div className="security-item">
|
||||
<h2>Métodos de Autorização</h2>
|
||||
<div className="auth-methods">
|
||||
<label className="checkbox-label">
|
||||
<div className="mb-5">
|
||||
<h2 className="text-xl font-semibold mb-2">Métodos de Autorização</h2>
|
||||
<form className="flex flex-col gap-2" onSubmit={handleSubmit}>
|
||||
<label className="flex items-center gap-2">
|
||||
RFID
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -46,7 +87,7 @@ const Security = () => {
|
||||
onChange={() => handleAuthMethodChange('RFID')}
|
||||
/>
|
||||
</label>
|
||||
<label className="checkbox-label">
|
||||
<label className="flex items-center gap-2">
|
||||
Aplicativo
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -54,7 +95,7 @@ const Security = () => {
|
||||
onChange={() => handleAuthMethodChange('App')}
|
||||
/>
|
||||
</label>
|
||||
<label className="checkbox-label">
|
||||
<label className="flex items-center gap-2">
|
||||
Senha
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -62,25 +103,56 @@ const Security = () => {
|
||||
onChange={() => handleAuthMethodChange('Password')}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 mt-4"
|
||||
>
|
||||
Salvar Configurações
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Utilizadores */}
|
||||
<div className="overflow-x-auto mb-5">
|
||||
<h2 className="text-xl font-semibold mb-4">Utilizadores</h2>
|
||||
<table className="min-w-full border border-gray-300 text-left table-auto">
|
||||
<thead className="bg-gray-100">
|
||||
<tr>
|
||||
<th className="border-b p-2 text-sm font-medium text-gray-700">Nome de Usuário</th>
|
||||
<th className="border-b p-2 text-sm font-medium text-gray-700">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user, index) => (
|
||||
<tr key={index} className="hover:bg-gray-50">
|
||||
<td className="border-b p-2 text-sm">{user.username}</td>
|
||||
<td className="border-b p-2 text-sm text-red-600">
|
||||
<button onClick={() => removeUser(user.username)}>Remover</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="mt-4">
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full mb-2"
|
||||
value={newUser}
|
||||
onChange={(e) => setNewUser(e.target.value)} // Atualiza o valor do novo nome de usuário
|
||||
placeholder="Digite o nome de usuário"
|
||||
/>
|
||||
<button
|
||||
className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 w-full"
|
||||
onClick={addUser}
|
||||
>
|
||||
Adicionar Novo Usuário
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Usuários */}
|
||||
<div className="security-item">
|
||||
<h2>Usuários</h2>
|
||||
<ul>
|
||||
{users.map((user, index) => (
|
||||
<li key={index}>
|
||||
<span>{user.username} - {user.role}</span>
|
||||
<button onClick={() => removeUser(user.username)}>Remover</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="add-user">
|
||||
<button onClick={() => addUser('newuser', 'User')}>Adicionar Novo Usuário</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// src/pages/Settings.jsx
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PageLayout from '../components/PageLayout';
|
||||
import Alert from '../components/Alert';
|
||||
|
||||
const Settings = () => {
|
||||
// Estados para armazenar os valores dos sliders e caixas de entrada
|
||||
@@ -8,82 +9,130 @@ const Settings = () => {
|
||||
const [energyLimit, setEnergyLimit] = useState(0);
|
||||
const [chargingTimeLimit, setChargingTimeLimit] = useState(0);
|
||||
const [temperatureLimit, setTemperatureLimit] = useState(60);
|
||||
const [msg, setMsg] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleCurrentLimitChange = (e) => setCurrentLimit(e.target.value);
|
||||
const handlePowerLimitChange = (e) => setPowerLimit(e.target.value);
|
||||
const handleEnergyLimitChange = (e) => setEnergyLimit(e.target.value);
|
||||
const handleChargingTimeLimitChange = (e) => setChargingTimeLimit(e.target.value);
|
||||
const handleTemperatureLimitChange = (e) => setTemperatureLimit(e.target.value);
|
||||
// Função para preencher os campos com os dados do servidor
|
||||
const fetchSettings = async () => {
|
||||
const response = await fetch('/api/v1/config/settings');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCurrentLimit(data.currentLimit);
|
||||
setPowerLimit(data.powerLimit);
|
||||
setEnergyLimit(data.energyLimit);
|
||||
setChargingTimeLimit(data.chargingTimeLimit);
|
||||
setTemperatureLimit(data.temperatureLimit);
|
||||
} else {
|
||||
setError('Erro ao obter as configurações');
|
||||
}
|
||||
};
|
||||
|
||||
// Carregar as configurações ao montar o componente
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
const handleCurrentLimitChange = (e) => setCurrentLimit(parseInt(e.target.value, 10));
|
||||
const handlePowerLimitChange = (e) => setPowerLimit(parseInt(e.target.value, 10));
|
||||
const handleEnergyLimitChange = (e) => setEnergyLimit(parseInt(e.target.value, 10));
|
||||
const handleChargingTimeLimitChange = (e) => setChargingTimeLimit(parseInt(e.target.value, 10));
|
||||
const handleTemperatureLimitChange = (e) => setTemperatureLimit(parseInt(e.target.value, 10));
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const settingsData = {
|
||||
currentLimit,
|
||||
powerLimit,
|
||||
energyLimit,
|
||||
chargingTimeLimit,
|
||||
temperatureLimit,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/config/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(settingsData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setMsg('Alterações guardadas com sucesso!');
|
||||
} else {
|
||||
setError('Erro ao guardar alterações.');
|
||||
}
|
||||
} catch {
|
||||
setError('Erro ao guardar alterações.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="settings-container">
|
||||
<h1 className="settings-title">Configurações Gerais</h1>
|
||||
<PageLayout title="Definições de Energia">
|
||||
{msg && <Alert type="success">{msg}</Alert>}
|
||||
{error && <Alert type="error">{error}</Alert>}
|
||||
|
||||
<div className="settings-item">
|
||||
<label>Corrente Máxima de Carregamento (A):</label>
|
||||
<div className="slider-container">
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="currentLimit">Limite de Corrente (A):</label>
|
||||
<input
|
||||
type="range"
|
||||
min="5"
|
||||
max="32"
|
||||
id="currentLimit"
|
||||
type="number"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={currentLimit}
|
||||
onChange={handleCurrentLimitChange}
|
||||
/>
|
||||
<span>{currentLimit} A</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label>Limite de Potência Máxima (W):</label>
|
||||
<div className="slider-container">
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="powerLimit">Limite de Potência (W):</label>
|
||||
<input
|
||||
type="range"
|
||||
min="1000"
|
||||
max="10000"
|
||||
id="powerLimit"
|
||||
type="number"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={powerLimit}
|
||||
onChange={handlePowerLimitChange}
|
||||
/>
|
||||
<span>{powerLimit} W</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label>Limite de Consumo Total de Energia (kWh):</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="energyLimit">Limite de Energia (kWh):</label>
|
||||
<input
|
||||
id="energyLimit"
|
||||
type="number"
|
||||
min="0"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={energyLimit}
|
||||
onChange={handleEnergyLimitChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label>Limite de Tempo de Carregamento (h):</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="chargingTimeLimit">Tempo Máximo de Carregamento (min):</label>
|
||||
<input
|
||||
id="chargingTimeLimit"
|
||||
type="number"
|
||||
min="1"
|
||||
max="24"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={chargingTimeLimit}
|
||||
onChange={handleChargingTimeLimitChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label>Limite de Temperatura Máxima do EVSE (ºC):</label>
|
||||
<div className="slider-container">
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="temperatureLimit">Temperatura Máxima (°C):</label>
|
||||
<input
|
||||
type="range"
|
||||
min="60"
|
||||
max="80"
|
||||
id="temperatureLimit"
|
||||
type="number"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={temperatureLimit}
|
||||
onChange={handleTemperatureLimitChange}
|
||||
/>
|
||||
<span>{temperatureLimit} ºC</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="save-button">Salvar Configurações</button>
|
||||
<div>
|
||||
<button className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700" type="submit">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { get, post } from '../api';
|
||||
import PageLayout from '../components/PageLayout';
|
||||
import Alert from '../components/Alert';
|
||||
|
||||
export default function Wifi() {
|
||||
const [config, setConfig] = useState({ ssid: '', password: '' });
|
||||
@@ -36,24 +37,25 @@ export default function Wifi() {
|
||||
const save = async () => {
|
||||
try {
|
||||
await post('/api/v1/config/wifi', config);
|
||||
setMsg('Configuração gravada com sucesso!');
|
||||
setMsg('Alterações guardadas com sucesso!');
|
||||
} catch {
|
||||
setMsg('Erro ao gravar.');
|
||||
setMsg('Erro ao guardar alterações.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout title="Configuração Wi-Fi">
|
||||
{msg && <div className="message">{msg}</div>}
|
||||
{msg && <Alert type={msg.startsWith('Erro') ? 'error' : 'success'}>{msg}</Alert>}
|
||||
|
||||
{loading ? (
|
||||
<p>A carregar...</p>
|
||||
) : (
|
||||
<form className="form" onSubmit={(e) => { e.preventDefault(); save(); }}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="ssid">SSID:</label>
|
||||
<form className="flex flex-col gap-4" onSubmit={(e) => { e.preventDefault(); save(); }}>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="ssid">SSID:</label>
|
||||
<select
|
||||
id="ssid"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={config.ssid}
|
||||
onChange={e => setConfig({ ...config, ssid: e.target.value })}
|
||||
>
|
||||
@@ -64,18 +66,19 @@ export default function Wifi() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Palavra-passe:</label>
|
||||
<div>
|
||||
<label className="block mb-1" htmlFor="password">Palavra-passe:</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
className="border border-gray-300 rounded px-3 py-2 w-full"
|
||||
value={config.password}
|
||||
onChange={e => setConfig({ ...config, password: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="button-grid">
|
||||
<button type="submit">Guardar</button>
|
||||
<div>
|
||||
<button className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700" type="submit">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
11
tailwind.config.js
Executable file
11
tailwind.config.js
Executable file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -7,10 +7,16 @@ export default defineConfig({
|
||||
port: 5173, // ou outro, se necessário
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
target: 'http://127.0.0.1:8080', // Alvo para redirecionar as requisições
|
||||
changeOrigin: true, // Garante que a origem seja alterada para o alvo do proxy
|
||||
secure: false // Desabilita a validação de SSL (útil para desenvolvimento em HTTP)
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist', // Defina o diretório de saída (padrão é 'dist')
|
||||
minify: 'esbuild', // Usa o esbuild para minimizar o código para produção
|
||||
sourcemap: false, // Desativa a criação de mapas de fonte (se não for necessário)
|
||||
// Outras opções de otimização de produção podem ser configuradas aqui, se necessário
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user