feat: initial commit - CertConverter PFX & AppWPP (React)
This commit is contained in:
@@ -0,0 +1,566 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Server,
|
||||
Plus,
|
||||
Settings,
|
||||
Play,
|
||||
List,
|
||||
Trash2,
|
||||
Search,
|
||||
Smartphone,
|
||||
Globe,
|
||||
Activity,
|
||||
MessageSquare,
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Info,
|
||||
Edit2
|
||||
} from 'lucide-react';
|
||||
import { getStoredServers, saveServers } from '../../utils/appWppStorage';
|
||||
import {
|
||||
startSession,
|
||||
closeSession,
|
||||
logoutSession,
|
||||
checkSessionStatus,
|
||||
clearSessionData,
|
||||
generateToken,
|
||||
ensureQRPrefix
|
||||
} from '../../utils/wppConnectApi';
|
||||
import WppSessionCard from './WppSessionCard';
|
||||
import ModalServer from './ModalServer';
|
||||
import ModalSession from './ModalSession';
|
||||
import ModalQRCode from './ModalQRCode';
|
||||
import ModalListSessions from './ModalListSessions';
|
||||
import ModalSettings from './ModalSettings';
|
||||
import ModalConfirm from './ModalConfirm';
|
||||
|
||||
export default function AppWPP() {
|
||||
const [servers, setServers] = useState([]);
|
||||
const [activeServerId, setActiveServerId] = useState(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [toasts, setToasts] = useState([]);
|
||||
|
||||
// Modals state
|
||||
const [isServerModalOpen, setIsServerModalOpen] = useState(false);
|
||||
const [serverToEdit, setServerToEdit] = useState(null);
|
||||
|
||||
const [isSessionModalOpen, setIsSessionModalOpen] = useState(false);
|
||||
const [sessionToEdit, setSessionToEdit] = useState(null);
|
||||
|
||||
const [isQRModalOpen, setIsQRModalOpen] = useState(false);
|
||||
const [activeQRSession, setActiveQRSession] = useState(null);
|
||||
|
||||
const [isListSessionsOpen, setIsListSessionsOpen] = useState(false);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
|
||||
// Styled Confirm Modal state
|
||||
const [confirmModal, setConfirmModal] = useState({
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
description: '',
|
||||
confirmText: 'Confirmar',
|
||||
type: 'warning',
|
||||
onConfirm: () => {}
|
||||
});
|
||||
|
||||
// Carrega servidores armazenados na montagem
|
||||
useEffect(() => {
|
||||
const list = getStoredServers();
|
||||
setServers(list);
|
||||
if (list.length > 0) {
|
||||
setActiveServerId(list[0].id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Salva no localStorage sempre que `servers` mudar
|
||||
const updateServersState = (updatedList) => {
|
||||
setServers(updatedList);
|
||||
saveServers(updatedList);
|
||||
};
|
||||
|
||||
const showToast = (message, type = 'info') => {
|
||||
const id = Date.now().toString() + Math.random().toString();
|
||||
setToasts((prev) => [...prev, { id, message, type }]);
|
||||
setTimeout(() => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||
}, 3500);
|
||||
};
|
||||
|
||||
const activeServer = servers.find((s) => s.id === activeServerId) || null;
|
||||
|
||||
// Servidor CRUD
|
||||
const handleSaveServer = (serverData) => {
|
||||
let updated;
|
||||
const exists = servers.some((s) => s.id === serverData.id);
|
||||
if (exists) {
|
||||
updated = servers.map((s) => (s.id === serverData.id ? { ...s, ...serverData } : s));
|
||||
showToast(`Servidor "${serverData.name}" atualizado!`, 'success');
|
||||
} else {
|
||||
updated = [...servers, serverData];
|
||||
setActiveServerId(serverData.id);
|
||||
showToast(`Servidor "${serverData.name}" adicionado com sucesso!`, 'success');
|
||||
}
|
||||
updateServersState(updated);
|
||||
};
|
||||
|
||||
const handleRemoveServer = () => {
|
||||
if (!activeServer) return;
|
||||
setConfirmModal({
|
||||
isOpen: true,
|
||||
title: 'Remover Servidor',
|
||||
message: `Tem certeza que deseja remover o servidor "${activeServer.name}"?`,
|
||||
description: 'Todas as sessões cadastradas para este servidor deixarão de ser exibidas na listagem.',
|
||||
confirmText: 'Remover Servidor',
|
||||
type: 'danger',
|
||||
onConfirm: () => {
|
||||
const updated = servers.filter((s) => s.id !== activeServer.id);
|
||||
updateServersState(updated);
|
||||
setActiveServerId(updated.length > 0 ? updated[0].id : null);
|
||||
showToast(`Servidor "${activeServer.name}" removido.`, 'info');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Sessão CRUD
|
||||
const handleSaveSession = async (sessionData, originalName) => {
|
||||
if (!activeServer) return;
|
||||
|
||||
let updatedSessions = [...(activeServer.sessions || [])];
|
||||
if (originalName) {
|
||||
updatedSessions = updatedSessions.map((s) => (s.name === originalName ? sessionData : s));
|
||||
showToast(`Sessão "${sessionData.name}" atualizada!`, 'success');
|
||||
} else {
|
||||
if (updatedSessions.some((s) => s.name === sessionData.name)) {
|
||||
showToast('Já existe uma sessão com este nome.', 'error');
|
||||
return;
|
||||
}
|
||||
updatedSessions.push(sessionData);
|
||||
showToast(`Sessão "${sessionData.name}" criada!`, 'success');
|
||||
}
|
||||
|
||||
const updatedServer = { ...activeServer, sessions: updatedSessions };
|
||||
const updatedServers = servers.map((s) => (s.id === activeServer.id ? updatedServer : s));
|
||||
updateServersState(updatedServers);
|
||||
|
||||
// Se for nova sessão, tenta gerar token e iniciar
|
||||
if (!originalName) {
|
||||
try {
|
||||
const token = await generateToken(activeServer, sessionData.name);
|
||||
if (token) {
|
||||
sessionData.token = token;
|
||||
updateServersState(
|
||||
servers.map((s) =>
|
||||
s.id === activeServer.id
|
||||
? { ...s, sessions: updatedSessions.map((x) => (x.name === sessionData.name ? { ...x, token } : x)) }
|
||||
: s
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (e) {}
|
||||
handleStartSession(sessionData.name, true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveSession = (sessionName) => {
|
||||
if (!activeServer) return;
|
||||
setConfirmModal({
|
||||
isOpen: true,
|
||||
title: 'Excluir Sessão',
|
||||
message: `Deseja realmente excluir a sessão "${sessionName}"?`,
|
||||
description: 'Esta sessão será removida do gerenciador para este servidor.',
|
||||
confirmText: 'Excluir Sessão',
|
||||
type: 'danger',
|
||||
onConfirm: () => {
|
||||
const updatedSessions = activeServer.sessions.filter((s) => s.name !== sessionName);
|
||||
const updatedServers = servers.map((s) =>
|
||||
s.id === activeServer.id ? { ...s, sessions: updatedSessions } : s
|
||||
);
|
||||
updateServersState(updatedServers);
|
||||
showToast(`Sessão "${sessionName}" removida.`, 'info');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Ações de Sessão
|
||||
const handleStartSession = async (sessionName, openQR = true) => {
|
||||
if (!activeServer) return;
|
||||
|
||||
// Atualiza estado local para connecting
|
||||
updateSessionProperty(sessionName, { status: 'connecting' });
|
||||
|
||||
if (openQR) {
|
||||
setActiveQRSession(sessionName);
|
||||
setIsQRModalOpen(true);
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await startSession(activeServer, sessionName);
|
||||
if ((data.status || '').toLowerCase() === 'qrcode' && (data.qrcode || data.urlcode)) {
|
||||
const qr = ensureQRPrefix(data.qrcode || data.urlcode);
|
||||
updateSessionProperty(sessionName, { qrCode: qr, status: 'qr' });
|
||||
showToast(`Escaneie o QR Code para "${sessionName}".`, 'warning');
|
||||
} else if (data.status === 'connected') {
|
||||
updateSessionProperty(sessionName, { status: 'connected', connectedAt: new Date().toLocaleString('pt-BR') });
|
||||
showToast(`Sessão "${sessionName}" conectada com sucesso!`, 'success');
|
||||
} else {
|
||||
updateSessionProperty(sessionName, { status: 'qr' });
|
||||
showToast(`Aguardando QR Code para "${sessionName}".`, 'warning');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Start session error:', err);
|
||||
updateSessionProperty(sessionName, { status: 'qr' });
|
||||
showToast(`Aguardando leitura do QR Code para "${sessionName}".`, 'warning');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseSession = async (sessionName) => {
|
||||
if (!activeServer) return;
|
||||
try {
|
||||
await closeSession(activeServer, sessionName);
|
||||
updateSessionProperty(sessionName, { status: 'disconnected' });
|
||||
showToast(`Sessão "${sessionName}" fechada.`, 'success');
|
||||
} catch (err) {
|
||||
showToast(`Erro ao fechar sessão: ${err.message}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogoutSession = async (sessionName) => {
|
||||
if (!activeServer) return;
|
||||
try {
|
||||
await logoutSession(activeServer, sessionName);
|
||||
updateSessionProperty(sessionName, { status: 'disconnected' });
|
||||
showToast(`Sessão "${sessionName}" desconectada.`, 'success');
|
||||
} catch (err) {
|
||||
showToast(`Erro ao desconectar: ${err.message}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckStatus = async (sessionName) => {
|
||||
if (!activeServer) return;
|
||||
try {
|
||||
const status = await checkSessionStatus(activeServer, sessionName);
|
||||
const isConnected = status === true || status === 'connected';
|
||||
const newStatus = isConnected ? 'connected' : 'disconnected';
|
||||
updateSessionProperty(sessionName, {
|
||||
status: newStatus,
|
||||
connectedAt: isConnected ? new Date().toLocaleString('pt-BR') : undefined
|
||||
});
|
||||
showToast(`Status de "${sessionName}": ${newStatus}`, isConnected ? 'success' : 'warning');
|
||||
} catch (err) {
|
||||
showToast(`Erro ao verificar status: ${err.message}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearSession = (sessionName) => {
|
||||
if (!activeServer) return;
|
||||
setConfirmModal({
|
||||
isOpen: true,
|
||||
title: 'Limpar Dados da Sessão',
|
||||
message: `Limpar arquivos e dados de "${sessionName}" no servidor?`,
|
||||
description: 'Esta operação removerá os tokens e os arquivos em cache da sessão diretamente no servidor WPPConnect.',
|
||||
confirmText: 'Limpar Arquivos e Dados',
|
||||
type: 'warning',
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await clearSessionData(activeServer, sessionName);
|
||||
showToast(`Dados da sessão "${sessionName}" limpos com sucesso no servidor.`, 'success');
|
||||
} catch (err) {
|
||||
showToast(`Erro ao limpar dados: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleStartAll = async () => {
|
||||
if (!activeServer || !activeServer.sessions) return;
|
||||
showToast('Iniciando todas as sessões...', 'info');
|
||||
for (const session of activeServer.sessions) {
|
||||
if (session.status !== 'connected') {
|
||||
await handleStartSession(session.name, false);
|
||||
}
|
||||
}
|
||||
showToast('Processamento de inicialização concluído.', 'success');
|
||||
};
|
||||
|
||||
const updateSessionProperty = (sessionName, props) => {
|
||||
if (!activeServer) return;
|
||||
const updatedSessions = activeServer.sessions.map((s) =>
|
||||
s.name === sessionName ? { ...s, ...props } : s
|
||||
);
|
||||
const updatedServers = servers.map((srv) =>
|
||||
srv.id === activeServer.id ? { ...srv, sessions: updatedSessions } : srv
|
||||
);
|
||||
updateServersState(updatedServers);
|
||||
};
|
||||
|
||||
// Filtro de sessões
|
||||
const filteredSessions = (activeServer?.sessions || []).filter((s) =>
|
||||
s.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="wpp-app-container">
|
||||
{/* Sidebar de Servidores */}
|
||||
<aside className="wpp-sidebar">
|
||||
<div className="wpp-sidebar-header">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.625rem' }}>
|
||||
<div className="wpp-logo-badge">
|
||||
<MessageSquare size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="wpp-app-title">AppWPP</h2>
|
||||
<p className="wpp-app-sub">Gerenciador WPPConnect</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-icon-wpp"
|
||||
onClick={() => setIsSettingsOpen(true)}
|
||||
title="Configurações e Backup"
|
||||
>
|
||||
<Settings size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="wpp-servers-section">
|
||||
<div className="wpp-servers-header">
|
||||
<span>SERVIDORES</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-icon-add-server"
|
||||
onClick={() => {
|
||||
setServerToEdit(null);
|
||||
setIsServerModalOpen(true);
|
||||
}}
|
||||
title="Adicionar Servidor"
|
||||
>
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="wpp-servers-list">
|
||||
{servers.map((s) => (
|
||||
<div
|
||||
key={s.id}
|
||||
className={`wpp-server-item ${s.id === activeServerId ? 'wpp-server-active' : ''}`}
|
||||
onClick={() => setActiveServerId(s.id)}
|
||||
>
|
||||
<span className={`wpp-server-dot ${s.online ? 'dot-online' : 'dot-offline'}`} />
|
||||
<div className="wpp-server-info-col">
|
||||
<span className="wpp-server-name-text">{s.name}</span>
|
||||
<span className="wpp-server-url-text">{s.baseUrl}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-edit-server-mini"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setServerToEdit(s);
|
||||
setIsServerModalOpen(true);
|
||||
}}
|
||||
title="Editar servidor"
|
||||
>
|
||||
<Edit2 size={13} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{servers.length === 0 && (
|
||||
<div style={{ padding: '1.5rem 1rem', textAlign: 'center', color: '#64748b', fontSize: '0.8rem' }}>
|
||||
Nenhum servidor cadastrado. Clique no "+" para adicionar.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="wpp-sidebar-footer">
|
||||
<span>WPPConnect React v1.0.2</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Conteúdo Principal do Servidor Selecionado */}
|
||||
<main className="wpp-main-area">
|
||||
{activeServer ? (
|
||||
<div className="wpp-server-view">
|
||||
{/* Topbar do Servidor */}
|
||||
<div className="wpp-server-topbar">
|
||||
<div className="wpp-server-title-box">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||
<h2 className="wpp-view-server-name">{activeServer.name}</h2>
|
||||
<span className={`wpp-badge-status ${activeServer.online ? 'badge-online' : 'badge-offline'}`}>
|
||||
{activeServer.online ? 'Online' : 'Offline'}
|
||||
</span>
|
||||
</div>
|
||||
<span className="wpp-view-server-url">{activeServer.baseUrl}</span>
|
||||
</div>
|
||||
|
||||
<div className="wpp-server-actions-group">
|
||||
<button type="button" className="btn-wpp-secondary-top" onClick={handleStartAll}>
|
||||
<Play size={15} />
|
||||
<span>Iniciar Todas</span>
|
||||
</button>
|
||||
|
||||
<button type="button" className="btn-wpp-secondary-top" onClick={() => setIsListSessionsOpen(true)}>
|
||||
<List size={15} />
|
||||
<span>Listar no Servidor</span>
|
||||
</button>
|
||||
|
||||
<button type="button" className="btn-wpp-danger-top" onClick={handleRemoveServer}>
|
||||
<Trash2 size={15} />
|
||||
<span>Remover Servidor</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toolbar de Sessões (Busca e Nova Sessão) */}
|
||||
<div className="wpp-sessions-toolbar">
|
||||
<div className="wpp-search-box">
|
||||
<Search size={16} className="text-muted" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar sessão..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="wpp-search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn-new-session"
|
||||
onClick={() => {
|
||||
setSessionToEdit(null);
|
||||
setIsSessionModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus size={16} />
|
||||
<span>Nova Sessão</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Grid de Cards de Sessões */}
|
||||
<div className="wpp-sessions-grid">
|
||||
{filteredSessions.map((session) => (
|
||||
<WppSessionCard
|
||||
key={session.name}
|
||||
session={session}
|
||||
onStart={handleStartSession}
|
||||
onCloseSession={handleCloseSession}
|
||||
onLogout={handleLogoutSession}
|
||||
onCheckStatus={handleCheckStatus}
|
||||
onEdit={(s) => {
|
||||
setSessionToEdit(s);
|
||||
setIsSessionModalOpen(true);
|
||||
}}
|
||||
onClear={handleClearSession}
|
||||
onRemove={handleRemoveSession}
|
||||
onShowQR={(name) => {
|
||||
setActiveQRSession(name);
|
||||
setIsQRModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{filteredSessions.length === 0 && (
|
||||
<div className="wpp-empty-sessions">
|
||||
<Smartphone size={48} className="text-muted" />
|
||||
<h3>Nenhuma sessão encontrada</h3>
|
||||
<p>
|
||||
{searchQuery
|
||||
? 'Nenhuma sessão corresponde à busca informada.'
|
||||
: 'Clique em "+ Nova Sessão" acima para cadastrar sua primeira sessão do WhatsApp.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="wpp-empty-app">
|
||||
<Smartphone size={64} className="text-emerald" />
|
||||
<h2>Bem-vindo ao AppWPP</h2>
|
||||
<p>Selecione ou adicione um servidor WPPConnect para gerenciar suas sessões do WhatsApp.</p>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-convert"
|
||||
onClick={() => {
|
||||
setServerToEdit(null);
|
||||
setIsServerModalOpen(true);
|
||||
}}
|
||||
style={{ marginTop: '1rem', padding: '0.75rem 1.5rem' }}
|
||||
>
|
||||
Adicionar Servidor WPPConnect
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Modals */}
|
||||
<ModalServer
|
||||
isOpen={isServerModalOpen}
|
||||
onClose={() => setIsServerModalOpen(false)}
|
||||
onSave={handleSaveServer}
|
||||
serverToEdit={serverToEdit}
|
||||
/>
|
||||
|
||||
<ModalSession
|
||||
isOpen={isSessionModalOpen}
|
||||
onClose={() => setIsSessionModalOpen(false)}
|
||||
onSave={handleSaveSession}
|
||||
sessionToEdit={sessionToEdit}
|
||||
/>
|
||||
|
||||
<ModalQRCode
|
||||
isOpen={isQRModalOpen}
|
||||
onClose={() => setIsQRModalOpen(false)}
|
||||
server={activeServer}
|
||||
sessionName={activeQRSession}
|
||||
onConnected={(name) => {
|
||||
updateSessionProperty(name, { status: 'connected', connectedAt: new Date().toLocaleString('pt-BR') });
|
||||
showToast(`Sessão "${name}" conectada com sucesso!`, 'success');
|
||||
}}
|
||||
/>
|
||||
|
||||
<ModalListSessions
|
||||
isOpen={isListSessionsOpen}
|
||||
onClose={() => setIsListSessionsOpen(false)}
|
||||
server={activeServer}
|
||||
/>
|
||||
|
||||
<ModalSettings
|
||||
isOpen={isSettingsOpen}
|
||||
onClose={() => setIsSettingsOpen(false)}
|
||||
servers={servers}
|
||||
onImportServers={(newServers) => {
|
||||
updateServersState(newServers);
|
||||
if (newServers.length > 0) setActiveServerId(newServers[0].id);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Modal de Confirmação Estilizado */}
|
||||
<ModalConfirm
|
||||
isOpen={confirmModal.isOpen}
|
||||
onClose={() => setConfirmModal((prev) => ({ ...prev, isOpen: false }))}
|
||||
onConfirm={confirmModal.onConfirm}
|
||||
title={confirmModal.title}
|
||||
message={confirmModal.message}
|
||||
description={confirmModal.description}
|
||||
confirmText={confirmModal.confirmText}
|
||||
type={confirmModal.type}
|
||||
/>
|
||||
|
||||
{/* Toasts */}
|
||||
<div className="wpp-toasts-container">
|
||||
{toasts.map((t) => (
|
||||
<div key={t.id} className={`wpp-toast toast-${t.type}`}>
|
||||
{t.type === 'success' && <CheckCircle2 size={16} />}
|
||||
{t.type === 'error' && <AlertCircle size={16} />}
|
||||
{t.type === 'warning' && <AlertCircle size={16} />}
|
||||
{t.type === 'info' && <Info size={16} />}
|
||||
<span>{t.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { X, AlertTriangle, Trash2, ShieldAlert } from 'lucide-react';
|
||||
|
||||
export default function ModalConfirm({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
title = 'Confirmar Ação',
|
||||
message,
|
||||
description,
|
||||
confirmText = 'Confirmar',
|
||||
cancelText = 'Cancelar',
|
||||
type = 'warning', // 'warning' | 'danger'
|
||||
icon: CustomIcon
|
||||
}) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const isDanger = type === 'danger';
|
||||
const IconComponent = CustomIcon || (isDanger ? Trash2 : AlertTriangle);
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={onClose}>
|
||||
<div
|
||||
className="modal-dialog modal-confirm-dialog"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ maxWidth: '460px' }}
|
||||
>
|
||||
<div className="modal-header">
|
||||
<div className="modal-title-box">
|
||||
<div className={`modal-confirm-icon-box ${isDanger ? 'icon-box-danger' : 'icon-box-warning'}`}>
|
||||
<IconComponent size={20} />
|
||||
</div>
|
||||
<h3 className="modal-title">{title}</h3>
|
||||
</div>
|
||||
<button type="button" className="modal-close-btn" onClick={onClose} title="Fechar">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body" style={{ padding: '1.5rem' }}>
|
||||
<p className="modal-confirm-message">{message}</p>
|
||||
{description && (
|
||||
<p className="modal-confirm-desc">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="modal-footer" style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-modal-cancel"
|
||||
onClick={onClose}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`btn-modal-action-confirm ${isDanger ? 'btn-confirm-danger' : 'btn-confirm-warning'}`}
|
||||
onClick={() => {
|
||||
onConfirm();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<IconComponent size={16} />
|
||||
<span>{confirmText}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { X, Server, Loader2, RefreshCw } from 'lucide-react';
|
||||
import { listAllServerSessions } from '../../utils/wppConnectApi';
|
||||
|
||||
export default function ModalListSessions({ isOpen, onClose, server }) {
|
||||
const [sessions, setSessions] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && server) {
|
||||
loadSessions();
|
||||
}
|
||||
}, [isOpen, server]);
|
||||
|
||||
const loadSessions = async () => {
|
||||
if (!server) return;
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const list = await listAllServerSessions(server);
|
||||
setSessions(list);
|
||||
} catch (err) {
|
||||
setError(err.message || 'Erro ao consultar sessões no servidor.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={onClose}>
|
||||
<div className="modal-dialog" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '580px' }}>
|
||||
<div className="modal-header">
|
||||
<div className="modal-title-box">
|
||||
<Server size={20} className="text-emerald" />
|
||||
<h3 className="modal-title">Sessões no Servidor ({server?.name})</h3>
|
||||
</div>
|
||||
<button type="button" className="modal-close-btn" onClick={onClose}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body" style={{ minHeight: '200px', maxHeight: '60vh', overflowY: 'auto' }}>
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '3rem 0', gap: '0.75rem' }}>
|
||||
<Loader2 size={32} className="animate-spin text-emerald" />
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.85rem' }}>Consultando servidor WPPConnect...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="alert-error-box">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<p style={{ textAlign: 'center', color: '#94a3b8', padding: '2rem 0' }}>
|
||||
Nenhuma sessão ativa encontrada diretamente neste servidor.
|
||||
</p>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
{sessions.map((sessName, idx) => {
|
||||
const localSession = server.sessions?.find(s => s.name === sessName);
|
||||
const isConnected = localSession?.status === 'connected';
|
||||
return (
|
||||
<div key={idx} className="wpp-server-session-row">
|
||||
<span style={{ fontWeight: 600, color: '#f8fafc', fontFamily: 'var(--font-mono)' }}>{sessName}</span>
|
||||
<span className={`wpp-status-pill ${isConnected ? 'status-connected' : 'status-disconnected'}`}>
|
||||
{isConnected ? 'Conectado' : 'Registrado'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="modal-footer" style={{ justifyContent: 'space-between' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-reset"
|
||||
onClick={loadSessions}
|
||||
disabled={loading}
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: '0.375rem' }}
|
||||
>
|
||||
<RefreshCw size={14} className={loading ? 'animate-spin' : ''} />
|
||||
<span>Recarregar</span>
|
||||
</button>
|
||||
<button type="button" className="btn-modal-close" onClick={onClose}>
|
||||
Fechar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { X, QrCode, RefreshCw, CheckCircle2, Loader2, Smartphone } from 'lucide-react';
|
||||
import { fetchQRCode, checkSessionStatus, ensureQRPrefix } from '../../utils/wppConnectApi';
|
||||
|
||||
export default function ModalQRCode({ isOpen, onClose, server, sessionName, onConnected }) {
|
||||
const [qrSrc, setQrSrc] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !server || !sessionName) return;
|
||||
|
||||
setIsConnected(false);
|
||||
const session = server.sessions?.find(s => s.name === sessionName);
|
||||
if (session?.qrCode) {
|
||||
setQrSrc(ensureQRPrefix(session.qrCode));
|
||||
}
|
||||
|
||||
// Carrega o QR Code inicial
|
||||
loadQR();
|
||||
|
||||
// 1. Polling para atualizar o QR Code a cada 25s
|
||||
const qrInterval = setInterval(() => {
|
||||
loadQR();
|
||||
}, 25000);
|
||||
|
||||
// 2. Polling rápido a cada 2.5s para checar se o usuário escaneou e conectou
|
||||
const statusInterval = setInterval(async () => {
|
||||
try {
|
||||
const status = await checkSessionStatus(server, sessionName);
|
||||
if (status === true || status === 'connected') {
|
||||
setIsConnected(true);
|
||||
clearInterval(statusInterval);
|
||||
clearInterval(qrInterval);
|
||||
if (onConnected) onConnected(sessionName);
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 1800);
|
||||
}
|
||||
} catch (e) {}
|
||||
}, 2500);
|
||||
|
||||
return () => {
|
||||
clearInterval(qrInterval);
|
||||
clearInterval(statusInterval);
|
||||
};
|
||||
}, [isOpen, server, sessionName]);
|
||||
|
||||
const loadQR = async () => {
|
||||
if (!server || !sessionName) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const qr = await fetchQRCode(server, sessionName);
|
||||
if (qr) setQrSrc(qr);
|
||||
} catch (e) {
|
||||
console.warn('Erro ao carregar QR Code:', e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={onClose}>
|
||||
<div className="modal-dialog" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '420px', textAlign: 'center' }}>
|
||||
<div className="modal-header">
|
||||
<div className="modal-title-box">
|
||||
<QrCode size={20} className="text-emerald" />
|
||||
<h3 className="modal-title">QR Code: {sessionName}</h3>
|
||||
</div>
|
||||
<button type="button" className="modal-close-btn" onClick={onClose}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '2rem 1.5rem' }}>
|
||||
{isConnected ? (
|
||||
<div style={{ padding: '2rem 0', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem' }}>
|
||||
<CheckCircle2 size={56} className="text-emerald" />
|
||||
<h4 style={{ color: '#34d399', fontSize: '1.2rem', fontWeight: 600 }}>WhatsApp Conectado!</h4>
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.85rem' }}>A sessão foi autenticada com sucesso.</p>
|
||||
</div>
|
||||
) : qrSrc ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<div className="wpp-qr-box">
|
||||
<img src={qrSrc} alt="QR Code WhatsApp" style={{ width: '240px', height: '240px', borderRadius: '8px', display: 'block' }} />
|
||||
</div>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', marginTop: '1rem', color: '#94a3b8', fontSize: '0.85rem' }}>
|
||||
<Smartphone size={16} />
|
||||
<span>Abra o WhatsApp > Dispositivos Conectados > Conectar</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: '2.5rem 1rem', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
|
||||
<Loader2 size={36} className="animate-spin text-emerald" />
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.9rem' }}>Aguardando o servidor gerar o QR Code...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="modal-footer" style={{ justifyContent: 'space-between' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-reset"
|
||||
onClick={loadQR}
|
||||
disabled={isLoading || isConnected}
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: '0.375rem', fontSize: '0.8rem' }}
|
||||
>
|
||||
<RefreshCw size={14} className={isLoading ? 'animate-spin' : ''} />
|
||||
<span>Atualizar QR Code</span>
|
||||
</button>
|
||||
<button type="button" className="btn-modal-close" onClick={onClose}>
|
||||
Fechar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Server, Eye, EyeOff, Check, AlertCircle, Loader2 } from 'lucide-react';
|
||||
import { testServerConnection, normalizeUrl } from '../../utils/wppConnectApi';
|
||||
|
||||
export default function ModalServer({ isOpen, onClose, onSave, serverToEdit }) {
|
||||
const [name, setName] = useState('');
|
||||
const [baseUrl, setBaseUrl] = useState('');
|
||||
const [secretKey, setSecretKey] = useState('');
|
||||
const [showSecret, setShowSecret] = useState(false);
|
||||
const [testStatus, setTestStatus] = useState(null); // { loading, success, message, error }
|
||||
|
||||
useEffect(() => {
|
||||
if (serverToEdit) {
|
||||
setName(serverToEdit.name || '');
|
||||
setBaseUrl(serverToEdit.baseUrl || '');
|
||||
setSecretKey(serverToEdit.secretKey || '');
|
||||
} else {
|
||||
setName('');
|
||||
setBaseUrl('');
|
||||
setSecretKey('');
|
||||
}
|
||||
setTestStatus(null);
|
||||
}, [serverToEdit, isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleTest = async () => {
|
||||
if (!baseUrl || !secretKey) {
|
||||
setTestStatus({ error: 'Preencha a URL e a Secret Key antes de testar.' });
|
||||
return;
|
||||
}
|
||||
setTestStatus({ loading: true });
|
||||
try {
|
||||
const res = await testServerConnection(baseUrl, secretKey);
|
||||
if (res.ok) {
|
||||
setTestStatus({ success: true, message: res.message });
|
||||
} else {
|
||||
setTestStatus({ error: res.error });
|
||||
}
|
||||
} catch (err) {
|
||||
setTestStatus({ error: err.message || 'Falha na conexão com o servidor' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = (e) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim() || !baseUrl.trim() || !secretKey.trim()) {
|
||||
alert('Por favor, preencha todos os campos obrigatórios.');
|
||||
return;
|
||||
}
|
||||
onSave({
|
||||
id: serverToEdit ? serverToEdit.id : Date.now().toString(),
|
||||
name: name.trim(),
|
||||
baseUrl: normalizeUrl(baseUrl),
|
||||
secretKey: secretKey.trim(),
|
||||
online: serverToEdit ? serverToEdit.online : false,
|
||||
sessions: serverToEdit ? serverToEdit.sessions : []
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={onClose}>
|
||||
<div className="modal-dialog" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '520px' }}>
|
||||
<div className="modal-header">
|
||||
<div className="modal-title-box">
|
||||
<Server size={20} className="text-emerald" />
|
||||
<h3 className="modal-title">{serverToEdit ? 'Editar Servidor' : 'Adicionar Servidor WPPConnect'}</h3>
|
||||
</div>
|
||||
<button type="button" className="modal-close-btn" onClick={onClose}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSave}>
|
||||
<div className="modal-body" style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Nome do Servidor</label>
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="Ex: C2 Sistemas"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">URL Base</label>
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="http://localhost:21465 ou http://meuservidor.com:22700"
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Secret Key / Token</label>
|
||||
<div className="password-input-wrapper">
|
||||
<input
|
||||
type={showSecret ? 'text' : 'password'}
|
||||
className="password-input"
|
||||
placeholder="Secret Key configurada no WPPConnect"
|
||||
value={secretKey}
|
||||
onChange={(e) => setSecretKey(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="password-toggle-btn"
|
||||
onClick={() => setShowSecret(!showSecret)}
|
||||
>
|
||||
{showSecret ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{testStatus && (
|
||||
<div className={`wpp-test-result ${testStatus.error ? 'wpp-test-error' : testStatus.success ? 'wpp-test-success' : 'wpp-test-loading'}`}>
|
||||
{testStatus.loading && <Loader2 size={16} className="animate-spin" />}
|
||||
{testStatus.success && <Check size={16} />}
|
||||
{testStatus.error && <AlertCircle size={16} />}
|
||||
<span>{testStatus.loading ? 'Testando conexão...' : testStatus.message || testStatus.error}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="modal-footer" style={{ justifyContent: 'space-between' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-reset"
|
||||
onClick={handleTest}
|
||||
disabled={testStatus?.loading}
|
||||
>
|
||||
Testar Conexão
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<button type="button" className="btn-modal-close" onClick={onClose}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" className="btn-convert" style={{ padding: '0.55rem 1.25rem' }}>
|
||||
{serverToEdit ? 'Atualizar' : 'Salvar Servidor'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, MessageSquare, Key, Globe, Shield } from 'lucide-react';
|
||||
|
||||
export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit }) {
|
||||
const [name, setName] = useState('');
|
||||
const [token, setToken] = useState('');
|
||||
const [webhook, setWebhook] = useState('');
|
||||
const [waitQrCode, setWaitQrCode] = useState(false);
|
||||
const [proxyUrl, setProxyUrl] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionToEdit) {
|
||||
setName(sessionToEdit.name || '');
|
||||
setToken(sessionToEdit.token || '');
|
||||
setWebhook(sessionToEdit.webhook || '');
|
||||
setWaitQrCode(sessionToEdit.waitQrCode || false);
|
||||
setProxyUrl(sessionToEdit.proxy?.url || '');
|
||||
} else {
|
||||
setName('');
|
||||
setToken('');
|
||||
setWebhook('');
|
||||
setWaitQrCode(false);
|
||||
setProxyUrl('');
|
||||
}
|
||||
}, [sessionToEdit, isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) {
|
||||
alert('Informe o nome da sessão.');
|
||||
return;
|
||||
}
|
||||
|
||||
onSave({
|
||||
name: name.trim(),
|
||||
token: token.trim(),
|
||||
webhook: webhook.trim(),
|
||||
waitQrCode,
|
||||
proxy: proxyUrl.trim() ? { url: proxyUrl.trim() } : null,
|
||||
status: sessionToEdit ? sessionToEdit.status : 'disconnected',
|
||||
connectedAt: sessionToEdit ? sessionToEdit.connectedAt : '',
|
||||
qrCode: sessionToEdit ? sessionToEdit.qrCode : ''
|
||||
}, sessionToEdit?.name);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={onClose}>
|
||||
<div className="modal-dialog" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '520px' }}>
|
||||
<div className="modal-header">
|
||||
<div className="modal-title-box">
|
||||
<MessageSquare size={20} className="text-emerald" />
|
||||
<h3 className="modal-title">{sessionToEdit ? 'Editar Sessão' : 'Nova Sessão WhatsApp'}</h3>
|
||||
</div>
|
||||
<button type="button" className="modal-close-btn" onClick={onClose}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="modal-body" style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Nome da Sessão (Identificador)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="Ex: atendimento_01, admin, clinica"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Token de Autenticação (Opcional)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="Token JWT gerado pelo servidor"
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Webhook URL (Opcional)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="https://meusite.com/api/wpp-webhook"
|
||||
value={webhook}
|
||||
onChange={(e) => setWebhook(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group" style={{ display: 'flex', alignItems: 'center', gap: '0.625rem' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="chk-wait-qr"
|
||||
checked={waitQrCode}
|
||||
onChange={(e) => setWaitQrCode(e.target.checked)}
|
||||
style={{ width: '18px', height: '18px', accentColor: '#00e676' }}
|
||||
/>
|
||||
<label htmlFor="chk-wait-qr" style={{ fontSize: '0.875rem', color: '#cbd5e1', cursor: 'pointer' }}>
|
||||
Aguardar leitura do QR Code na inicialização
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Proxy URL (Opcional)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="http://proxy.exemplo.com:8080"
|
||||
value={proxyUrl}
|
||||
onChange={(e) => setProxyUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn-modal-close" onClick={onClose}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" className="btn-convert" style={{ padding: '0.55rem 1.25rem' }}>
|
||||
{sessionToEdit ? 'Atualizar Sessão' : 'Criar Sessão'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { X, Settings, Download, Upload, Database, Check, AlertCircle } from 'lucide-react';
|
||||
import { getSupabaseConfig, saveSupabaseConfig, exportAppWppBackup } from '../../utils/appWppStorage';
|
||||
|
||||
export default function ModalSettings({ isOpen, onClose, servers, onImportServers }) {
|
||||
const [supabaseUrl, setSupabaseUrl] = useState('');
|
||||
const [supabaseKey, setSupabaseKey] = useState('');
|
||||
const [msg, setMsg] = useState(null);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const cfg = getSupabaseConfig();
|
||||
setSupabaseUrl(cfg.supabaseUrl || '');
|
||||
setSupabaseKey(cfg.supabaseAnonKey || '');
|
||||
setMsg(null);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSaveSupabase = (e) => {
|
||||
e.preventDefault();
|
||||
saveSupabaseConfig({ supabaseUrl: supabaseUrl.trim(), supabaseAnonKey: supabaseKey.trim() });
|
||||
setMsg({ type: 'success', text: 'Configurações salvas localmente com sucesso!' });
|
||||
setTimeout(() => setMsg(null), 3000);
|
||||
};
|
||||
|
||||
const handleExportBackup = () => {
|
||||
exportAppWppBackup(servers);
|
||||
setMsg({ type: 'success', text: 'Backup exportado com sucesso!' });
|
||||
};
|
||||
|
||||
const handleImportFile = (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
const json = JSON.parse(event.target.result);
|
||||
if (json && Array.isArray(json.servers)) {
|
||||
onImportServers(json.servers);
|
||||
setMsg({ type: 'success', text: `${json.servers.length} servidor(es) importado(s) com sucesso!` });
|
||||
} else {
|
||||
setMsg({ type: 'error', text: 'Formato inválido. O arquivo JSON deve conter um array "servers".' });
|
||||
}
|
||||
} catch (err) {
|
||||
setMsg({ type: 'error', text: 'Erro ao processar arquivo JSON: ' + err.message });
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={onClose}>
|
||||
<div className="modal-dialog" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '540px' }}>
|
||||
<div className="modal-header">
|
||||
<div className="modal-title-box">
|
||||
<Settings size={20} className="text-emerald" />
|
||||
<h3 className="modal-title">Configurações & Backup (AppWPP)</h3>
|
||||
</div>
|
||||
<button type="button" className="modal-close-btn" onClick={onClose}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body" style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||
{msg && (
|
||||
<div className={`wpp-test-result ${msg.type === 'success' ? 'wpp-test-success' : 'wpp-test-error'}`}>
|
||||
{msg.type === 'success' ? <Check size={16} /> : <AlertCircle size={16} />}
|
||||
<span>{msg.text}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Backup e Sincronização Local */}
|
||||
<div className="wpp-settings-card">
|
||||
<h4 style={{ color: '#f8fafc', fontSize: '0.95rem', marginBottom: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<Download size={16} className="text-cyan" />
|
||||
<span>Backup e Importação (appwpp-data.json)</span>
|
||||
</h4>
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.8rem', marginBottom: '1rem' }}>
|
||||
Exporte seus servidores e sessões para transferir entre máquinas ou importe um arquivo existente.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<button type="button" className="btn-reset" onClick={handleExportBackup} style={{ display: 'inline-flex', alignItems: 'center', gap: '0.375rem' }}>
|
||||
<Download size={15} />
|
||||
<span>Exportar Backup (.json)</span>
|
||||
</button>
|
||||
<button type="button" className="btn-reset" onClick={() => fileInputRef.current?.click()} style={{ display: 'inline-flex', alignItems: 'center', gap: '0.375rem' }}>
|
||||
<Upload size={15} />
|
||||
<span>Importar Backup (.json)</span>
|
||||
</button>
|
||||
<input type="file" ref={fileInputRef} onChange={handleImportFile} accept=".json" style={{ display: 'none' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Supabase Config */}
|
||||
<form onSubmit={handleSaveSupabase} className="wpp-settings-card">
|
||||
<h4 style={{ color: '#f8fafc', fontSize: '0.95rem', marginBottom: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<Database size={16} className="text-emerald" />
|
||||
<span>Conexão Supabase (Opcional)</span>
|
||||
</h4>
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.8rem', marginBottom: '1rem' }}>
|
||||
Configure a sincronização com banco de dados Supabase na nuvem.
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Supabase URL</label>
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="https://seuprojeto.supabase.co"
|
||||
value={supabaseUrl}
|
||||
onChange={(e) => setSupabaseUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Anon Key / API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
className="password-input"
|
||||
placeholder="eyJhbGciOi..."
|
||||
value={supabaseKey}
|
||||
onChange={(e) => setSupabaseKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn-convert" style={{ alignSelf: 'flex-start', padding: '0.5rem 1rem', fontSize: '0.85rem' }}>
|
||||
Salvar Configurações
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn-modal-close" onClick={onClose}>
|
||||
Fechar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Play,
|
||||
X,
|
||||
LogOut,
|
||||
Activity,
|
||||
Edit3,
|
||||
Trash2,
|
||||
QrCode,
|
||||
Clock,
|
||||
Key,
|
||||
Copy,
|
||||
Check
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function WppSessionCard({
|
||||
session,
|
||||
onStart,
|
||||
onCloseSession,
|
||||
onLogout,
|
||||
onCheckStatus,
|
||||
onEdit,
|
||||
onClear,
|
||||
onRemove,
|
||||
onShowQR
|
||||
}) {
|
||||
const [copiedToken, setCopiedToken] = React.useState(false);
|
||||
|
||||
const getStatusBadge = () => {
|
||||
switch (session.status) {
|
||||
case 'connected':
|
||||
return <span className="wpp-status-pill status-connected">Conectado</span>;
|
||||
case 'qr':
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="wpp-status-pill status-qr clickable"
|
||||
onClick={() => onShowQR(session.name)}
|
||||
title="Clique para ver o QR Code"
|
||||
>
|
||||
<QrCode size={12} />
|
||||
<span>Aguardando QR</span>
|
||||
</button>
|
||||
);
|
||||
case 'connecting':
|
||||
return <span className="wpp-status-pill status-connecting">Conectando...</span>;
|
||||
default:
|
||||
return <span className="wpp-status-pill status-disconnected">Desconectado</span>;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyToken = () => {
|
||||
if (!session.token) return;
|
||||
navigator.clipboard.writeText(session.token);
|
||||
setCopiedToken(true);
|
||||
setTimeout(() => setCopiedToken(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="wpp-session-card">
|
||||
<div className="wpp-session-card-header">
|
||||
<span className="wpp-session-name" title={session.name}>{session.name}</span>
|
||||
{getStatusBadge()}
|
||||
</div>
|
||||
|
||||
<div className="wpp-session-card-body">
|
||||
{session.connectedAt && (
|
||||
<div className="wpp-session-meta-item">
|
||||
<Clock size={13} />
|
||||
<span>{session.connectedAt}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{session.token && (
|
||||
<div className="wpp-session-token-box" onClick={handleCopyToken} title="Clique para copiar o token">
|
||||
<Key size={12} />
|
||||
<span className="wpp-session-token-text">{session.token.slice(0, 18)}...</span>
|
||||
{copiedToken ? <Check size={12} className="text-emerald" /> : <Copy size={12} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="wpp-session-card-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn-wpp-action btn-wpp-primary"
|
||||
onClick={() => onStart(session.name)}
|
||||
title="Iniciar sessão e abrir QR Code"
|
||||
>
|
||||
<Play size={13} />
|
||||
<span>Iniciar</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn-wpp-action btn-wpp-secondary"
|
||||
onClick={() => onCloseSession(session.name)}
|
||||
title="Fechar sessão"
|
||||
>
|
||||
<X size={13} />
|
||||
<span>Fechar</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn-wpp-action btn-wpp-secondary"
|
||||
onClick={() => onLogout(session.name)}
|
||||
title="Desconectar sessão do WhatsApp"
|
||||
>
|
||||
<LogOut size={13} />
|
||||
<span>Sair</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn-wpp-action btn-wpp-secondary"
|
||||
onClick={() => onCheckStatus(session.name)}
|
||||
title="Consultar status no servidor"
|
||||
>
|
||||
<Activity size={13} />
|
||||
<span>Status</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn-wpp-action btn-wpp-secondary"
|
||||
onClick={() => onEdit(session)}
|
||||
title="Editar dados da sessão"
|
||||
>
|
||||
<Edit3 size={13} />
|
||||
<span>Editar</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn-wpp-action btn-wpp-warning"
|
||||
onClick={() => onClear(session.name)}
|
||||
title="Limpar arquivos da sessão no servidor"
|
||||
>
|
||||
<Trash2 size={13} />
|
||||
<span>Limpar</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn-wpp-action btn-wpp-danger"
|
||||
onClick={() => onRemove(session.name)}
|
||||
title="Excluir sessão do gerenciador"
|
||||
>
|
||||
<Trash2 size={13} />
|
||||
<span>Excluir</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user