424 lines
14 KiB
React
424 lines
14 KiB
React
import React, { useState, useEffect, useRef } from 'react';
|
||
import {
|
||
Bookmark,
|
||
Search,
|
||
Plus,
|
||
Share2,
|
||
Download,
|
||
Upload,
|
||
RotateCcw,
|
||
Check,
|
||
Copy,
|
||
LayoutGrid,
|
||
Sparkles,
|
||
CheckCircle2,
|
||
AlertCircle,
|
||
Info,
|
||
Lock,
|
||
Unlock
|
||
} from 'lucide-react';
|
||
import {
|
||
CATEGORIES,
|
||
INITIAL_PORTAL_ITEMS,
|
||
getStoredPortalItems,
|
||
savePortalItems,
|
||
generateWhatsAppDescriptionText
|
||
} from '../../utils/customerPortalData';
|
||
import PortalItemCard from './PortalItemCard';
|
||
import ModalPortalItem from './ModalPortalItem';
|
||
import ModalConfirm from '../wpp/ModalConfirm';
|
||
import ModalSupportAuth from './ModalSupportAuth';
|
||
|
||
export default function CustomerPortal() {
|
||
const [items, setItems] = useState([]);
|
||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||
const [searchQuery, setSearchQuery] = useState('');
|
||
const [copiedWhatsapp, setCopiedWhatsapp] = useState(false);
|
||
const [toasts, setToasts] = useState([]);
|
||
|
||
// Estado de Autenticação da Equipe de Suporte
|
||
const [isSupportUnlocked, setIsSupportUnlocked] = useState(false);
|
||
const [authModal, setAuthModal] = useState({
|
||
isOpen: false,
|
||
title: '',
|
||
onSuccess: null
|
||
});
|
||
|
||
// Modals state
|
||
const [isModalItemOpen, setIsModalItemOpen] = useState(false);
|
||
const [itemToEdit, setItemToEdit] = useState(null);
|
||
|
||
const [confirmModal, setConfirmModal] = useState({
|
||
isOpen: false,
|
||
title: '',
|
||
message: '',
|
||
description: '',
|
||
confirmText: 'Confirmar',
|
||
type: 'danger',
|
||
onConfirm: () => {}
|
||
});
|
||
|
||
const fileInputRef = useRef(null);
|
||
|
||
useEffect(() => {
|
||
setItems(getStoredPortalItems());
|
||
}, []);
|
||
|
||
const updateItemsState = (newItems) => {
|
||
setItems(newItems);
|
||
savePortalItems(newItems);
|
||
};
|
||
|
||
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);
|
||
};
|
||
|
||
// Solicitação de autenticação de suporte para ver senhas
|
||
const handleRequestSupportAuth = (onSuccess, title) => {
|
||
setAuthModal({
|
||
isOpen: true,
|
||
title: title || 'este acesso',
|
||
onSuccess
|
||
});
|
||
};
|
||
|
||
const handleSessionUnlock = () => {
|
||
setIsSupportUnlocked(true);
|
||
showToast('Modo de suporte autenticado para esta sessão!', 'success');
|
||
};
|
||
|
||
const handleLockSupport = () => {
|
||
setIsSupportUnlocked(false);
|
||
showToast('Modo de suporte bloqueado. Senhas protegidas.', 'info');
|
||
};
|
||
|
||
// Salvar Item (Criar ou Editar)
|
||
const handleSaveItem = (itemData) => {
|
||
let updated;
|
||
const exists = items.some(i => i.id === itemData.id);
|
||
if (exists) {
|
||
updated = items.map(i => i.id === itemData.id ? itemData : i);
|
||
showToast(`Acesso "${itemData.title}" atualizado!`, 'success');
|
||
} else {
|
||
updated = [itemData, ...items];
|
||
showToast(`Acesso "${itemData.title}" adicionado!`, 'success');
|
||
}
|
||
updateItemsState(updated);
|
||
};
|
||
|
||
// Excluir Item com Modal Confirm Estilizado
|
||
const handleDeleteItem = (item) => {
|
||
setConfirmModal({
|
||
isOpen: true,
|
||
title: 'Excluir Acesso',
|
||
message: `Deseja realmente remover "${item.title}" da sua Central de Acessos?`,
|
||
description: 'Este atalho deixará de ser exibido na lista e nos backups.',
|
||
confirmText: 'Excluir Acesso',
|
||
type: 'danger',
|
||
onConfirm: () => {
|
||
const updated = items.filter(i => i.id !== item.id);
|
||
updateItemsState(updated);
|
||
showToast(`Acesso "${item.title}" removido.`, 'info');
|
||
}
|
||
});
|
||
};
|
||
|
||
// Restaurar dados originais
|
||
const handleResetDefaults = () => {
|
||
setConfirmModal({
|
||
isOpen: true,
|
||
title: 'Restaurar Acessos Originais',
|
||
message: 'Deseja restaurar a lista com todos os 23 acessos padrões do grupo de WhatsApp?',
|
||
description: 'Itens personalizados que você tiver adicionado serão substituídos pela lista padrão original.',
|
||
confirmText: 'Restaurar Padrão',
|
||
type: 'warning',
|
||
onConfirm: () => {
|
||
updateItemsState(INITIAL_PORTAL_ITEMS);
|
||
showToast('Lista restaurada com sucesso!', 'success');
|
||
}
|
||
});
|
||
};
|
||
|
||
// Copiar texto pronto para o WhatsApp
|
||
const handleCopyWhatsAppText = () => {
|
||
const text = generateWhatsAppDescriptionText(items);
|
||
navigator.clipboard.writeText(text);
|
||
setCopiedWhatsapp(true);
|
||
showToast('Texto completo copiado para colar no WhatsApp!', 'success');
|
||
setTimeout(() => setCopiedWhatsapp(false), 2500);
|
||
};
|
||
|
||
// Exportar Backup JSON
|
||
const handleExportJson = () => {
|
||
const data = JSON.stringify(items, null, 2);
|
||
const blob = new Blob([data], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `central-de-acessos-backup-${new Date().toISOString().slice(0, 10)}.json`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
showToast('Backup JSON exportado com sucesso!', 'success');
|
||
};
|
||
|
||
// Importar Backup JSON
|
||
const handleImportJson = (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 (Array.isArray(json)) {
|
||
updateItemsState(json);
|
||
showToast(`${json.length} acessos importados com sucesso!`, 'success');
|
||
} else {
|
||
showToast('Formato inválido. O arquivo JSON deve ser uma lista de acessos.', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Erro ao processar JSON: ' + err.message, 'error');
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
};
|
||
|
||
// Filtro por Categoria e Busca
|
||
const filteredItems = items.filter(item => {
|
||
const matchesCategory = selectedCategory === 'all' || item.category === selectedCategory;
|
||
const q = searchQuery.toLowerCase().trim();
|
||
if (!q) return matchesCategory;
|
||
|
||
const matchesSearch =
|
||
(item.title && item.title.toLowerCase().includes(q)) ||
|
||
(item.url && item.url.toLowerCase().includes(q)) ||
|
||
(item.secondaryUrl && item.secondaryUrl.toLowerCase().includes(q)) ||
|
||
(item.username && item.username.toLowerCase().includes(q)) ||
|
||
(item.ipHost && item.ipHost.toLowerCase().includes(q)) ||
|
||
(item.description && item.description.toLowerCase().includes(q)) ||
|
||
(item.wifiNetwork && item.wifiNetwork.toLowerCase().includes(q)) ||
|
||
(item.tags && item.tags.some(t => t.toLowerCase().includes(q)));
|
||
|
||
return matchesCategory && matchesSearch;
|
||
});
|
||
|
||
return (
|
||
<div className="portal-container">
|
||
{/* Top Banner & Header */}
|
||
<div className="portal-header-card">
|
||
<div className="portal-header-left">
|
||
<div className="portal-header-icon-box">
|
||
<Bookmark size={26} />
|
||
</div>
|
||
<div>
|
||
<h2 className="portal-header-title">Central de Acessos</h2>
|
||
<p className="portal-header-sub">
|
||
Todos os links, logins, IPs de servidores e acessos centralizados do seu grupo de WhatsApp.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="portal-header-actions">
|
||
<button
|
||
type="button"
|
||
className="btn-whatsapp-export"
|
||
onClick={handleCopyWhatsAppText}
|
||
title="Copiar lista formatada para a descrição do WhatsApp"
|
||
>
|
||
{copiedWhatsapp ? <Check size={16} /> : <Share2 size={16} />}
|
||
<span>{copiedWhatsapp ? 'Copiado para WhatsApp!' : 'Copiar p/ WhatsApp'}</span>
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
className="btn-portal-new"
|
||
onClick={() => {
|
||
setItemToEdit(null);
|
||
setIsModalItemOpen(true);
|
||
}}
|
||
>
|
||
<Plus size={16} />
|
||
<span>Novo Acesso</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Toolbar: Busca, Categorias e Ferramentas */}
|
||
<div className="portal-toolbar">
|
||
{/* Campo de Busca */}
|
||
<div className="portal-search-box">
|
||
<Search size={16} className="text-muted" />
|
||
<input
|
||
type="text"
|
||
placeholder="Buscar por nome, URL, IP ou login..."
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
className="portal-search-input"
|
||
/>
|
||
{searchQuery && (
|
||
<button type="button" className="btn-clear-search" onClick={() => setSearchQuery('')}>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Ferramentas de Backup e Restauração */}
|
||
<div className="portal-tools-group">
|
||
<button
|
||
type="button"
|
||
className="btn-portal-tool"
|
||
onClick={handleExportJson}
|
||
title="Exportar backup em formato JSON"
|
||
>
|
||
<Download size={14} />
|
||
<span>Exportar JSON</span>
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
className="btn-portal-tool"
|
||
onClick={() => fileInputRef.current?.click()}
|
||
title="Importar backup JSON"
|
||
>
|
||
<Upload size={14} />
|
||
<span>Importar</span>
|
||
</button>
|
||
<input type="file" ref={fileInputRef} onChange={handleImportJson} accept=".json" style={{ display: 'none' }} />
|
||
|
||
<button
|
||
type="button"
|
||
className="btn-portal-tool"
|
||
onClick={handleResetDefaults}
|
||
title="Restaurar os 23 acessos padrões originais"
|
||
>
|
||
<RotateCcw size={14} />
|
||
<span>Restaurar Padrão</span>
|
||
</button>
|
||
|
||
{/* Botão de Autenticação / Status do Suporte */}
|
||
{isSupportUnlocked ? (
|
||
<button
|
||
type="button"
|
||
className="btn-portal-tool"
|
||
style={{ color: '#38bdf8', borderColor: '#0284c7', background: 'rgba(2, 132, 199, 0.15)' }}
|
||
onClick={handleLockSupport}
|
||
title="Modo suporte autenticado. Clique para bloquear."
|
||
>
|
||
<Unlock size={14} />
|
||
<span>Suporte Ativo (Bloquear)</span>
|
||
</button>
|
||
) : (
|
||
<button
|
||
type="button"
|
||
className="btn-portal-tool"
|
||
onClick={() => handleRequestSupportAuth(handleSessionUnlock, 'painel de suporte')}
|
||
title="Autenticar equipe de suporte para visualizar senhas"
|
||
>
|
||
<Lock size={14} />
|
||
<span>Autenticar Suporte</span>
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Filtro de Categorias */}
|
||
<div className="portal-categories-bar">
|
||
{CATEGORIES.map(cat => {
|
||
const count = cat.id === 'all'
|
||
? items.length
|
||
: items.filter(i => i.category === cat.id).length;
|
||
|
||
return (
|
||
<button
|
||
key={cat.id}
|
||
type="button"
|
||
className={`portal-cat-btn ${selectedCategory === cat.id ? 'portal-cat-active' : ''}`}
|
||
onClick={() => setSelectedCategory(cat.id)}
|
||
>
|
||
<span>{cat.label}</span>
|
||
<span className="cat-count-badge">{count}</span>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Grid de Cards de Acesso */}
|
||
<div className="portal-cards-grid">
|
||
{filteredItems.map(item => (
|
||
<PortalItemCard
|
||
key={item.id}
|
||
item={item}
|
||
isSupportUnlocked={isSupportUnlocked}
|
||
onRequestSupportAuth={handleRequestSupportAuth}
|
||
onEdit={(it) => {
|
||
setItemToEdit(it);
|
||
setIsModalItemOpen(true);
|
||
}}
|
||
onDelete={handleDeleteItem}
|
||
/>
|
||
))}
|
||
|
||
{filteredItems.length === 0 && (
|
||
<div className="portal-empty-state">
|
||
<LayoutGrid size={48} className="text-muted" />
|
||
<h3>Nenhum acesso encontrado</h3>
|
||
<p>
|
||
{searchQuery
|
||
? `Nenhum resultado corresponde à busca "${searchQuery}".`
|
||
: 'Nenhum acesso cadastrado nesta categoria. Clique em "+ Novo Acesso" para adicionar.'}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Modal de Criação / Edição de Acesso */}
|
||
<ModalPortalItem
|
||
isOpen={isModalItemOpen}
|
||
onClose={() => setIsModalItemOpen(false)}
|
||
onSave={handleSaveItem}
|
||
itemToEdit={itemToEdit}
|
||
/>
|
||
|
||
{/* 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}
|
||
/>
|
||
|
||
{/* Modal de Autenticação de Suporte (Senha Padrão C2s33131782*) */}
|
||
<ModalSupportAuth
|
||
isOpen={authModal.isOpen}
|
||
onClose={() => setAuthModal(prev => ({ ...prev, isOpen: false }))}
|
||
onSuccess={authModal.onSuccess}
|
||
targetTitle={authModal.title}
|
||
onSessionUnlock={handleSessionUnlock}
|
||
/>
|
||
|
||
{/* 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>
|
||
);
|
||
}
|