Files
InflavioApps/src/components/portal/CustomerPortal.jsx
T

424 lines
14 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}