feat: add Customer Portal (Área do Cliente & Central de Acessos) with 23 preloaded items and WhatsApp export
This commit is contained in:
+18
-8
@@ -7,7 +7,8 @@ import {
|
||||
Sparkles,
|
||||
Lock,
|
||||
Loader2,
|
||||
MessageSquare
|
||||
MessageSquare,
|
||||
Bookmark
|
||||
} from 'lucide-react';
|
||||
import Header from './components/Header';
|
||||
import SecurityBadge from './components/SecurityBadge';
|
||||
@@ -18,10 +19,11 @@ import OutputFiles from './components/OutputFiles';
|
||||
import OpenSslReference from './components/OpenSslReference';
|
||||
import HostGuideModal from './components/HostGuideModal';
|
||||
import AppWPP from './components/wpp/AppWPP';
|
||||
import CustomerPortal from './components/portal/CustomerPortal';
|
||||
import { convertPfx } from './utils/certConverter';
|
||||
|
||||
export default function App() {
|
||||
const [activeTab, setActiveTab] = useState('cert'); // 'cert' | 'wpp'
|
||||
const [activeTab, setActiveTab] = useState('portal'); // 'portal' | 'cert' | 'wpp' (default to portal since user asked for it!)
|
||||
const [file, setFile] = useState(null);
|
||||
const [arrayBuffer, setArrayBuffer] = useState(null);
|
||||
const [baseName, setBaseName] = useState('certificado');
|
||||
@@ -102,9 +104,15 @@ export default function App() {
|
||||
onOpenHostingGuide={() => setIsHostingGuideOpen(true)}
|
||||
/>
|
||||
|
||||
{/* Conteúdo Principal */}
|
||||
{/* Conteúdo Principal Dinâmico */}
|
||||
<main className="main-content">
|
||||
{activeTab === 'cert' ? (
|
||||
{activeTab === 'portal' && (
|
||||
/* Área do Cliente & Central de Acessos */
|
||||
<CustomerPortal />
|
||||
)}
|
||||
|
||||
{activeTab === 'cert' && (
|
||||
/* Conversor de Certificados PFX */
|
||||
<div className="container">
|
||||
{/* Banner de Segurança & Privacidade */}
|
||||
<SecurityBadge />
|
||||
@@ -201,7 +209,9 @@ export default function App() {
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
)}
|
||||
|
||||
{activeTab === 'wpp' && (
|
||||
/* Aplicativo WPPConnect Integrado */
|
||||
<div className="wpp-view-wrapper">
|
||||
<AppWPP />
|
||||
@@ -213,9 +223,9 @@ export default function App() {
|
||||
<footer className="footer">
|
||||
<div className="footer-content">
|
||||
<p>
|
||||
{activeTab === 'cert'
|
||||
? 'CertConverter Web — Conversão PKCS#12 / X.509 em conformidade com padrões ICP-Brasil e OpenSSL.'
|
||||
: 'AppWPP Web — Gerenciador de Sessões WPPConnect integrado ao portal de utilitários.'}
|
||||
{activeTab === 'portal' && 'InflavioApps — Área do Cliente & Central de Acessos C2 Sistemas.'}
|
||||
{activeTab === 'cert' && 'InflavioApps — Conversão PKCS#12 / X.509 em conformidade com padrões ICP-Brasil e OpenSSL.'}
|
||||
{activeTab === 'wpp' && 'InflavioApps — Gerenciador de Sessões WPPConnect integrado ao portal.'}
|
||||
</p>
|
||||
<div className="footer-links">
|
||||
<button
|
||||
|
||||
+54
-20
@@ -1,38 +1,62 @@
|
||||
import React from 'react';
|
||||
import { ShieldCheck, MessageSquare, Server, Layers } from 'lucide-react';
|
||||
import { ShieldCheck, MessageSquare, Bookmark, Server } from 'lucide-react';
|
||||
|
||||
export default function Header({ activeTab, onSelectTab, onOpenHostingGuide }) {
|
||||
const getBrandDetails = () => {
|
||||
switch (activeTab) {
|
||||
case 'cert':
|
||||
return {
|
||||
title: 'Conversor PFX',
|
||||
sub: 'Conversor de Certificados Digitais (.pfx para .crt, .pem, .key)',
|
||||
icon: ShieldCheck,
|
||||
iconClass: 'brand-icon-cert'
|
||||
};
|
||||
case 'wpp':
|
||||
return {
|
||||
title: 'AppWPP Manager',
|
||||
sub: 'Gerenciador de Sessões WPPConnect / WhatsApp',
|
||||
icon: MessageSquare,
|
||||
iconClass: 'brand-icon-wpp'
|
||||
};
|
||||
case 'portal':
|
||||
default:
|
||||
return {
|
||||
title: 'Área do Cliente',
|
||||
sub: 'Central de Links, Logins e Acessos Rápidos',
|
||||
icon: Bookmark,
|
||||
iconClass: 'brand-icon-portal'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const current = getBrandDetails();
|
||||
const Icon = current.icon;
|
||||
|
||||
return (
|
||||
<header className="header">
|
||||
<div className="header-container">
|
||||
<div className="header-brand">
|
||||
<div className="brand-icon-wrapper">
|
||||
{activeTab === 'cert' ? (
|
||||
<ShieldCheck className="brand-icon" size={26} />
|
||||
) : (
|
||||
<MessageSquare className="brand-icon" size={26} />
|
||||
)}
|
||||
<div className={`brand-icon-wrapper ${current.iconClass}`}>
|
||||
<Icon className="brand-icon" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="brand-title">
|
||||
{activeTab === 'cert' ? 'CertConverter PFX' : 'AppWPP Manager'}
|
||||
</h1>
|
||||
<p className="brand-subtitle">
|
||||
{activeTab === 'cert'
|
||||
? 'Conversor de Certificado Digital (.pfx para .crt, .pem, .key)'
|
||||
: 'Gerenciador de Sessões WPPConnect / WhatsApp'}
|
||||
</p>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
|
||||
<span className="brand-prefix">InflavioApps</span>
|
||||
<span className="brand-sep">/</span>
|
||||
<h1 className="brand-title">{current.title}</h1>
|
||||
</div>
|
||||
<p className="brand-subtitle">{current.sub}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Seletor Central de Aplicação */}
|
||||
{/* Seletor Central de Aplicações */}
|
||||
<div className="app-nav-tabs">
|
||||
<button
|
||||
type="button"
|
||||
className={`app-nav-btn ${activeTab === 'cert' ? 'app-nav-btn-active' : ''}`}
|
||||
onClick={() => onSelectTab('cert')}
|
||||
>
|
||||
<ShieldCheck size={18} />
|
||||
<ShieldCheck size={16} />
|
||||
<span>Conversor PFX</span>
|
||||
</button>
|
||||
|
||||
@@ -41,10 +65,20 @@ export default function Header({ activeTab, onSelectTab, onOpenHostingGuide }) {
|
||||
className={`app-nav-btn ${activeTab === 'wpp' ? 'app-nav-btn-active app-nav-btn-wpp' : ''}`}
|
||||
onClick={() => onSelectTab('wpp')}
|
||||
>
|
||||
<MessageSquare size={18} />
|
||||
<span>AppWPP (WhatsApp)</span>
|
||||
<MessageSquare size={16} />
|
||||
<span>AppWPP</span>
|
||||
<span className="nav-badge-pill">8</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`app-nav-btn ${activeTab === 'portal' ? 'app-nav-btn-active app-nav-btn-portal' : ''}`}
|
||||
onClick={() => onSelectTab('portal')}
|
||||
>
|
||||
<Bookmark size={16} />
|
||||
<span>Área do Cliente</span>
|
||||
<span className="nav-badge-pill pill-portal">23</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="header-actions">
|
||||
@@ -54,7 +88,7 @@ export default function Header({ activeTab, onSelectTab, onOpenHostingGuide }) {
|
||||
onClick={onOpenHostingGuide}
|
||||
title="Ver instruções de hospedagem da aplicação"
|
||||
>
|
||||
<Server size={18} />
|
||||
<Server size={17} />
|
||||
<span>Hospedagem</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Bookmark,
|
||||
Search,
|
||||
Plus,
|
||||
Share2,
|
||||
Download,
|
||||
Upload,
|
||||
RotateCcw,
|
||||
Check,
|
||||
Copy,
|
||||
LayoutGrid,
|
||||
Sparkles,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Info
|
||||
} 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';
|
||||
|
||||
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([]);
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
// 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 Área do Cliente?`,
|
||||
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 = `area-do-cliente-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">Área do Cliente & 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>
|
||||
</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}
|
||||
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}
|
||||
/>
|
||||
|
||||
{/* 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,219 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, BookmarkPlus, Globe, Key, User, Server, Wifi, FileText } from 'lucide-react';
|
||||
import { CATEGORIES } from '../../utils/customerPortalData';
|
||||
|
||||
export default function ModalPortalItem({ isOpen, onClose, onSave, itemToEdit }) {
|
||||
const [title, setTitle] = useState('');
|
||||
const [category, setCategory] = useState('clientes');
|
||||
const [url, setUrl] = useState('');
|
||||
const [secondaryUrl, setSecondaryUrl] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [ipHost, setIpHost] = useState('');
|
||||
const [wifiNetwork, setWifiNetwork] = useState('');
|
||||
const [wifiPassword, setWifiPassword] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (itemToEdit) {
|
||||
setTitle(itemToEdit.title || '');
|
||||
setCategory(itemToEdit.category || 'clientes');
|
||||
setUrl(itemToEdit.url || '');
|
||||
setSecondaryUrl(itemToEdit.secondaryUrl || '');
|
||||
setUsername(itemToEdit.username || '');
|
||||
setPassword(itemToEdit.password || '');
|
||||
setIpHost(itemToEdit.ipHost || '');
|
||||
setWifiNetwork(itemToEdit.wifiNetwork || '');
|
||||
setWifiPassword(itemToEdit.wifiPassword || '');
|
||||
setDescription(itemToEdit.description || '');
|
||||
} else {
|
||||
setTitle('');
|
||||
setCategory('clientes');
|
||||
setUrl('');
|
||||
setSecondaryUrl('');
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
setIpHost('');
|
||||
setWifiNetwork('');
|
||||
setWifiPassword('');
|
||||
setDescription('');
|
||||
}
|
||||
}, [itemToEdit, isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (!title.trim()) {
|
||||
alert('Informe o título do item.');
|
||||
return;
|
||||
}
|
||||
|
||||
onSave({
|
||||
id: itemToEdit ? itemToEdit.id : Date.now().toString(),
|
||||
title: title.trim(),
|
||||
category,
|
||||
url: url.trim(),
|
||||
secondaryUrl: secondaryUrl.trim(),
|
||||
username: username.trim(),
|
||||
password: password.trim(),
|
||||
ipHost: ipHost.trim(),
|
||||
wifiNetwork: wifiNetwork.trim(),
|
||||
wifiPassword: wifiPassword.trim(),
|
||||
description: description.trim()
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
const validCategories = CATEGORIES.filter(c => c.id !== 'all');
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={onClose}>
|
||||
<div className="modal-dialog" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '560px' }}>
|
||||
<div className="modal-header">
|
||||
<div className="modal-title-box">
|
||||
<BookmarkPlus size={20} className="text-cyan" />
|
||||
<h3 className="modal-title">{itemToEdit ? 'Editar Acesso / Link' : 'Novo Acesso ou Link'}</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: '0.875rem', maxHeight: '72vh', overflowY: 'auto' }}>
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Título / Nome do Sistema</label>
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="Ex: Área do Cliente SIGEF, Cobpag Web..."
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Categoria</label>
|
||||
<select
|
||||
className="password-input"
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{validCategories.map(cat => (
|
||||
<option key={cat.id} value={cat.id} style={{ background: '#111827', color: '#f8fafc' }}>
|
||||
{cat.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">URL Principal (Link de Acesso)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="https://exemplo.com.br"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">URL Secundária (Opcional)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="https://linkalternativo.com.br"
|
||||
value={secondaryUrl}
|
||||
onChange={(e) => setSecondaryUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem' }}>
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Usuário / Login (Opcional)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="admin ou email"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Senha (Opcional)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="Senha de acesso"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Host / IP Remoto / Servidor (Opcional)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="177.207.253.134:34089"
|
||||
value={ipHost}
|
||||
onChange={(e) => setIpHost(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem' }}>
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Rede Wi-Fi (Opcional)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="Nome do Wi-Fi"
|
||||
value={wifiNetwork}
|
||||
onChange={(e) => setWifiNetwork(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Senha Wi-Fi (Opcional)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="password-input"
|
||||
placeholder="Senha do Wi-Fi"
|
||||
value={wifiPassword}
|
||||
onChange={(e) => setWifiPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="wpp-form-label">Descrição / Observações (Opcional)</label>
|
||||
<textarea
|
||||
className="password-input"
|
||||
placeholder="Instruções de acesso, finalidade do sistema, etc."
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={2}
|
||||
style={{ resize: 'vertical' }}
|
||||
/>
|
||||
</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' }}>
|
||||
{itemToEdit ? 'Atualizar Acesso' : 'Adicionar Acesso'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
ExternalLink,
|
||||
Copy,
|
||||
Check,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Edit3,
|
||||
Trash2,
|
||||
Wifi,
|
||||
Server,
|
||||
User,
|
||||
Key,
|
||||
Globe
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function PortalItemCard({ item, onEdit, onDelete }) {
|
||||
const [copiedKey, setCopiedKey] = useState(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showWifiPassword, setShowWifiPassword] = useState(false);
|
||||
|
||||
const handleCopy = (text, key) => {
|
||||
if (!text) return;
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopiedKey(key);
|
||||
setTimeout(() => setCopiedKey(null), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="portal-card">
|
||||
<div className="portal-card-header">
|
||||
<div className="portal-title-group">
|
||||
<span className="portal-item-title">{item.title}</span>
|
||||
{item.description && (
|
||||
<p className="portal-item-desc">{item.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="portal-card-actions-top">
|
||||
<button
|
||||
type="button"
|
||||
className="btn-portal-icon-action"
|
||||
onClick={() => onEdit(item)}
|
||||
title="Editar acesso"
|
||||
>
|
||||
<Edit3 size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-portal-icon-action btn-portal-delete"
|
||||
onClick={() => onDelete(item)}
|
||||
title="Excluir acesso"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Seção de Links / URLs */}
|
||||
<div className="portal-links-area">
|
||||
{item.url && (
|
||||
<a
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="portal-url-btn"
|
||||
title={`Abrir ${item.url}`}
|
||||
>
|
||||
<Globe size={14} className="text-cyan" />
|
||||
<span className="portal-url-text">{item.url}</span>
|
||||
<ExternalLink size={13} className="portal-ext-icon" />
|
||||
</a>
|
||||
)}
|
||||
|
||||
{item.secondaryUrl && (
|
||||
<a
|
||||
href={item.secondaryUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="portal-url-btn portal-url-secondary"
|
||||
title={`Abrir link secundário: ${item.secondaryUrl}`}
|
||||
>
|
||||
<Globe size={14} className="text-emerald" />
|
||||
<span className="portal-url-text">{item.secondaryUrl}</span>
|
||||
<ExternalLink size={13} className="portal-ext-icon" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Credenciais e Informações Rápidas */}
|
||||
<div className="portal-credentials-grid">
|
||||
{/* Host / IP Remoto */}
|
||||
{item.ipHost && (
|
||||
<div className="portal-cred-pill" onClick={() => handleCopy(item.ipHost, 'ipHost')} title="Clique para copiar IP/Host">
|
||||
<Server size={13} className="text-cyan" />
|
||||
<span className="portal-cred-label">IP:</span>
|
||||
<span className="portal-cred-val font-mono">{item.ipHost}</span>
|
||||
{copiedKey === 'ipHost' ? <Check size={12} className="text-emerald" /> : <Copy size={12} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Usuário / Login */}
|
||||
{item.username && (
|
||||
<div className="portal-cred-pill" onClick={() => handleCopy(item.username, 'user')} title="Clique para copiar Usuário">
|
||||
<User size={13} className="text-cyan" />
|
||||
<span className="portal-cred-label">Login:</span>
|
||||
<span className="portal-cred-val">{item.username}</span>
|
||||
{copiedKey === 'user' ? <Check size={12} className="text-emerald" /> : <Copy size={12} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Senha */}
|
||||
{item.password && (
|
||||
<div className="portal-cred-pill" title="Clique para copiar a Senha">
|
||||
<Key size={13} className="text-amber" />
|
||||
<span className="portal-cred-label">Senha:</span>
|
||||
<span className="portal-cred-val font-mono" onClick={() => handleCopy(item.password, 'pass')}>
|
||||
{showPassword ? item.password : '••••••••'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-toggle-cred-pw"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowPassword(!showPassword);
|
||||
}}
|
||||
title={showPassword ? 'Ocultar' : 'Ver senha'}
|
||||
>
|
||||
{showPassword ? <EyeOff size={12} /> : <Eye size={12} />}
|
||||
</button>
|
||||
<span onClick={() => handleCopy(item.password, 'pass')} style={{ cursor: 'pointer', display: 'flex' }}>
|
||||
{copiedKey === 'pass' ? <Check size={12} className="text-emerald" /> : <Copy size={12} />}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Wi-Fi Rede */}
|
||||
{item.wifiNetwork && (
|
||||
<div className="portal-cred-pill" onClick={() => handleCopy(item.wifiNetwork, 'wifiNet')} title="Clique para copiar nome da Rede">
|
||||
<Wifi size={13} className="text-emerald" />
|
||||
<span className="portal-cred-label">Rede:</span>
|
||||
<span className="portal-cred-val">{item.wifiNetwork}</span>
|
||||
{copiedKey === 'wifiNet' ? <Check size={12} className="text-emerald" /> : <Copy size={12} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Wi-Fi Senha */}
|
||||
{item.wifiPassword && (
|
||||
<div className="portal-cred-pill" title="Clique para copiar senha Wi-Fi">
|
||||
<Key size={13} className="text-emerald" />
|
||||
<span className="portal-cred-label">Senha Wi-Fi:</span>
|
||||
<span className="portal-cred-val font-mono" onClick={() => handleCopy(item.wifiPassword, 'wifiPass')}>
|
||||
{showWifiPassword ? item.wifiPassword : '••••••••'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-toggle-cred-pw"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowWifiPassword(!showWifiPassword);
|
||||
}}
|
||||
title={showWifiPassword ? 'Ocultar' : 'Ver senha'}
|
||||
>
|
||||
{showWifiPassword ? <EyeOff size={12} /> : <Eye size={12} />}
|
||||
</button>
|
||||
<span onClick={() => handleCopy(item.wifiPassword, 'wifiPass')} style={{ cursor: 'pointer', display: 'flex' }}>
|
||||
{copiedKey === 'wifiPass' ? <Check size={12} className="text-emerald" /> : <Copy size={12} />}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+470
@@ -2147,4 +2147,474 @@ body {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
ÁREA DO CLIENTE & CENTRAL DE ACESSOS STYLES
|
||||
========================================================================= */
|
||||
|
||||
.brand-prefix {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
.brand-sep {
|
||||
color: #475569;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.brand-icon-cert {
|
||||
background: linear-gradient(135deg, #0ea5e9, #2563eb);
|
||||
}
|
||||
|
||||
.brand-icon-wpp {
|
||||
background: linear-gradient(135deg, #059669, #10b981);
|
||||
}
|
||||
|
||||
.brand-icon-portal {
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
}
|
||||
|
||||
.app-nav-btn-portal {
|
||||
color: #c084fc !important;
|
||||
}
|
||||
|
||||
.pill-portal {
|
||||
background-color: rgba(168, 85, 247, 0.2) !important;
|
||||
color: #c084fc !important;
|
||||
}
|
||||
|
||||
/* Portal Container */
|
||||
.portal-container {
|
||||
width: 100%;
|
||||
max-width: 1260px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Header Card */
|
||||
.portal-header-card {
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.25rem;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.portal-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.portal-header-icon-box {
|
||||
background: linear-gradient(135deg, #6366f1, #a855f7);
|
||||
color: white;
|
||||
padding: 0.65rem;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35);
|
||||
}
|
||||
|
||||
.portal-header-title {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.portal-header-sub {
|
||||
font-size: 0.85rem;
|
||||
color: #94a3b8;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.portal-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-whatsapp-export {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: linear-gradient(135deg, #059669, #10b981);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.65rem 1.25rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 4px 14px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.btn-whatsapp-export:hover {
|
||||
background: linear-gradient(135deg, #047857, #059669);
|
||||
box-shadow: 0 6px 18px rgba(16, 185, 129, 0.45);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-portal-new {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.65rem 1.25rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.btn-portal-new:hover {
|
||||
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
||||
box-shadow: 0 6px 18px rgba(99, 102, 241, 0.45);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
.portal-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.portal-search-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
padding: 0.6rem 1rem;
|
||||
min-width: 320px;
|
||||
flex: 1;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.portal-search-input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9rem;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn-clear-search {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
.btn-clear-search:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.portal-tools-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-portal-tool {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
padding: 0.5rem 0.85rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-portal-tool:hover {
|
||||
background-color: var(--bg-card-hover);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
|
||||
/* Categories Bar */
|
||||
.portal-categories-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 0.5rem;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.portal-cat-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
padding: 0.5rem 0.95rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.825rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.portal-cat-btn:hover {
|
||||
background-color: var(--bg-card-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.portal-cat-active {
|
||||
background-color: rgba(99, 102, 241, 0.15) !important;
|
||||
border-color: rgba(99, 102, 241, 0.4) !important;
|
||||
color: #a5b4fc !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cat-count-badge {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.7rem;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.portal-cat-active .cat-count-badge {
|
||||
background-color: #6366f1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Cards Grid */
|
||||
.portal-cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.portal-card {
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 14px;
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.portal-card:hover {
|
||||
border-color: var(--border-hover);
|
||||
background-color: rgba(26, 36, 58, 0.6);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.portal-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.portal-title-group {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.portal-item-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: #f8fafc;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.portal-item-desc {
|
||||
font-size: 0.8rem;
|
||||
color: #94a3b8;
|
||||
margin-top: 0.25rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.portal-card-actions-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-portal-icon-action {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 0.35rem;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-portal-icon-action:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-portal-delete:hover {
|
||||
background-color: rgba(244, 63, 94, 0.15);
|
||||
color: #fb7185;
|
||||
}
|
||||
|
||||
/* Links Area */
|
||||
.portal-links-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.portal-url-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: rgba(14, 165, 233, 0.08);
|
||||
border: 1px solid rgba(14, 165, 233, 0.25);
|
||||
color: #38bdf8;
|
||||
padding: 0.45rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.8rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.portal-url-btn:hover {
|
||||
background: rgba(14, 165, 233, 0.16);
|
||||
border-color: #38bdf8;
|
||||
color: #7dd3fc;
|
||||
}
|
||||
|
||||
.portal-url-secondary {
|
||||
background: rgba(16, 185, 129, 0.08);
|
||||
border-color: rgba(16, 185, 129, 0.25);
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
.portal-url-secondary:hover {
|
||||
background: rgba(16, 185, 129, 0.16);
|
||||
border-color: #34d399;
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
.portal-url-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.portal-ext-icon {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Credentials Pills */
|
||||
.portal-credentials-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: auto;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.portal-cred-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background-color: var(--bg-input);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 0.35rem 0.65rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.portal-cred-pill:hover {
|
||||
border-color: var(--border-hover);
|
||||
background-color: var(--bg-card-hover);
|
||||
}
|
||||
|
||||
.portal-cred-label {
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.portal-cred-val {
|
||||
color: #f8fafc;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-toggle-cred-pw {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 0.15rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.btn-toggle-cred-pw:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.portal-empty-state {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 5rem 1.5rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.portal-empty-state h3 {
|
||||
font-size: 1.15rem;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.portal-empty-state p {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
max-width: 440px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
const STORAGE_KEY = 'inflavio_customer_portal_data_v1';
|
||||
|
||||
export const CATEGORIES = [
|
||||
{ id: 'all', label: 'Todos os Acessos' },
|
||||
{ id: 'clientes', label: '👥 Clientes & Portais' },
|
||||
{ id: 'sistemas', label: '🚀 Sistemas C2 & Sigef' },
|
||||
{ id: 'financeiro', label: '💰 Financeiro & Cobrança' },
|
||||
{ id: 'fiscal', label: '📑 Fiscal & Contratos' },
|
||||
{ id: 'servidores', label: '🖥️ Servidores & Infra' },
|
||||
{ id: 'atendimento', label: '💬 Atendimento & Chat' },
|
||||
{ id: 'docs', label: '📁 Documentação & Mídia' },
|
||||
{ id: 'conectividade', label: '📶 Conectividade & Downloads' },
|
||||
];
|
||||
|
||||
export const INITIAL_PORTAL_ITEMS = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Área do Cliente (SIGEF)',
|
||||
category: 'clientes',
|
||||
url: 'https://app.sigefweb.com.br/novo/?page=paginacliente',
|
||||
secondaryUrl: 'https://sigefclientes.com.br/',
|
||||
username: '26535660010',
|
||||
password: '123',
|
||||
description: 'Portal de acesso principal e gerenciador de clientes SIGEF.',
|
||||
tags: ['Cliente', 'Portal', 'SIGEF']
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Acesso Remoto Servidor (RDP)',
|
||||
category: 'servidores',
|
||||
ipHost: '177.207.253.134:34089',
|
||||
description: 'Conexão remota (Área de Trabalho Remota / MSTSC) ao servidor principal.',
|
||||
tags: ['RDP', 'Servidor', 'Acesso Remoto']
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Cobpag Web',
|
||||
category: 'financeiro',
|
||||
url: 'https://cobpagweb.com.br/clientes/',
|
||||
username: 'admin',
|
||||
password: '5177',
|
||||
description: 'Plataforma web de pagamentos e boletos para clientes.',
|
||||
tags: ['Cobrança', 'Pagamentos', 'Financeiro']
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'C2 Web',
|
||||
category: 'sistemas',
|
||||
url: 'https://c2web.sigefweb.com.br/',
|
||||
description: 'Painel e retaguarda web da C2 Sistemas.',
|
||||
tags: ['C2', 'Sistemas', 'Web']
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'Nuvem Fiscal (Console)',
|
||||
category: 'fiscal',
|
||||
url: 'https://console.nuvemfiscal.com.br/#/empresas',
|
||||
username: 'c2sistemas@c2sistemas.com.br',
|
||||
password: 'C2s33131782*',
|
||||
description: 'Console da Nuvem Fiscal para consulta e gestão de emissão de NF-e, NFS-e e empresas.',
|
||||
tags: ['Fiscal', 'NF-e', 'NFS-e', 'Nuvem Fiscal']
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: 'Novo IP Liberação (Painel)',
|
||||
category: 'servidores',
|
||||
url: 'https://liberacao.sigefweb.com.br/',
|
||||
description: 'Portal de liberação de IP e acessos externos do SIGEF.',
|
||||
tags: ['Liberação', 'IP', 'Firewall']
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
title: 'WI-FI BrisaNet (Escritório)',
|
||||
category: 'conectividade',
|
||||
wifiNetwork: 'brisa-3731955',
|
||||
wifiPassword: 'dn8lrtwu',
|
||||
description: 'Rede sem fio e senha de conexão da BrisaNet.',
|
||||
tags: ['Wi-Fi', 'Rede', 'Internet']
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
title: 'Sigef Vendas',
|
||||
category: 'sistemas',
|
||||
url: 'https://vendas.sigefweb.com.br/inicio/teste',
|
||||
description: 'Módulo web de vendas e emissão comercial do SIGEF.',
|
||||
tags: ['Vendas', 'Comercial', 'SIGEF']
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
title: 'Trello (Testes SigefWeb)',
|
||||
category: 'docs',
|
||||
url: 'https://trello.com/b/N8pJ9UAB/testes-sigefweb',
|
||||
description: 'Quadro Kanban do Trello para acompanhamento de tarefas, bugs e testes do SigefWeb.',
|
||||
tags: ['Trello', 'Kanban', 'Tarefas', 'Testes']
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
title: 'Gerenciador Contratos Clientes',
|
||||
category: 'fiscal',
|
||||
url: 'https://sign.c2sistemas.com.br/',
|
||||
username: 'Email da Empresa',
|
||||
password: '123456',
|
||||
description: 'Plataforma C2 Sign para assinatura eletrônica e gestão de contratos.',
|
||||
tags: ['Contratos', 'Assinatura', 'Sign']
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
title: 'Vídeos SigefWeb (Google Drive)',
|
||||
category: 'docs',
|
||||
url: 'https://drive.google.com/drive/folders/1H3nxjKuXu2ZZ01r-ncXXQB4Z0xbE60ao',
|
||||
description: 'Pasta do Drive com tutoriais, treinamentos e vídeos explicativos do SigefWeb.',
|
||||
tags: ['Vídeos', 'Treinamento', 'Drive']
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
title: 'Registros SQLs (Planilha)',
|
||||
category: 'docs',
|
||||
url: 'https://docs.google.com/spreadsheets/d/1SgouMzhubJ1bgj2oW8Kv78jxwot9EZkC_hUmi4aRq40/edit?gid=0#gid=0',
|
||||
description: 'Planilha Google com histórico de comandos SQL, queries e rotinas de banco de dados.',
|
||||
tags: ['SQL', 'Banco de Dados', 'Planilha']
|
||||
},
|
||||
{
|
||||
id: '13',
|
||||
title: 'Novo Chat C2 (Atendimento)',
|
||||
category: 'atendimento',
|
||||
url: 'https://atende.c2sistemas.com.br/',
|
||||
description: 'Central de atendimento e suporte via chat da C2 Sistemas.',
|
||||
tags: ['Chat', 'Atendimento', 'Suporte']
|
||||
},
|
||||
{
|
||||
id: '14',
|
||||
title: 'Material de Publicidade & Marketing',
|
||||
category: 'docs',
|
||||
url: 'https://drive.google.com/drive/folders/1bGuo0EWlUu1R-sJ3-dxGEtgiwNAP7am7?usp=sharing',
|
||||
description: 'Pasta no Google Drive com artes, logotipos, banners e material de divulgação.',
|
||||
tags: ['Marketing', 'Publicidade', 'Design', 'Drive']
|
||||
},
|
||||
{
|
||||
id: '15',
|
||||
title: 'Códigos ISPB (Banco Central)',
|
||||
category: 'financeiro',
|
||||
url: 'https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.pdf',
|
||||
description: 'Tabela oficial do Banco Central com os códigos ISPB das instituições participantes do STR/Pix.',
|
||||
tags: ['ISPB', 'Bacen', 'Bancos', 'Financeiro']
|
||||
},
|
||||
{
|
||||
id: '16',
|
||||
title: 'Financeiro C2',
|
||||
category: 'financeiro',
|
||||
url: 'http://45.185.208.229:8888/',
|
||||
username: 'admin',
|
||||
password: 'a',
|
||||
description: 'Painel administrativo e gerencial do financeiro C2.',
|
||||
tags: ['Financeiro', 'Admin', 'Gestão']
|
||||
},
|
||||
{
|
||||
id: '17',
|
||||
title: 'Acompanhar Servidores Empresas',
|
||||
category: 'servidores',
|
||||
url: 'https://app.sigefweb.com.br/novo/SigefWeb.dll/server',
|
||||
description: 'Painel de monitoramento de instâncias e servidores ativos das empresas no SIGEF.',
|
||||
tags: ['Monitoramento', 'Servidores', 'Status']
|
||||
},
|
||||
{
|
||||
id: '18',
|
||||
title: 'AnyDesk (Download Mega)',
|
||||
category: 'conectividade',
|
||||
url: 'https://mega.nz/file/cQVRGBgb#qobEEUO3gej9KzouT4u_mdlaBqEra_UUb-IzzDoLmPY',
|
||||
description: 'Instalador rápido do AnyDesk para suporte remoto.',
|
||||
tags: ['AnyDesk', 'Suporte', 'Download']
|
||||
},
|
||||
{
|
||||
id: '19',
|
||||
title: 'Conversor de Certificado (Mega / Nativo)',
|
||||
category: 'conectividade',
|
||||
url: 'https://mega.nz/file/tZ1TVbjB#-GXPcjjJLl6U24QwQtjg4WRUE43e9RKKeY9QiRKWNBU',
|
||||
description: 'Download do executável de conversão (agora também embutido na aba "Conversor PFX" do InflavioApps).',
|
||||
tags: ['Certificado', 'PFX', 'Download']
|
||||
},
|
||||
{
|
||||
id: '20',
|
||||
title: 'Consultar Prefeitura NFS-e (ACBr)',
|
||||
category: 'fiscal',
|
||||
url: 'https://acbr.api.br/consulta-nfse/',
|
||||
description: 'Consulta da API ACBr para verificar provedores e padrões de NFS-e por prefeitura/cidade.',
|
||||
tags: ['NFS-e', 'ACBr', 'Prefeitura', 'Fiscal']
|
||||
},
|
||||
{
|
||||
id: '21',
|
||||
title: 'IP Liberação (Endereço Fixo)',
|
||||
category: 'servidores',
|
||||
ipHost: '179.127.2.106',
|
||||
description: 'Endereço IP para cadastro em white-lists e liberação de portas de servidores.',
|
||||
tags: ['IP', 'Liberação', 'Infra']
|
||||
},
|
||||
{
|
||||
id: '22',
|
||||
title: 'Senha Agendamento Chatwoot',
|
||||
category: 'atendimento',
|
||||
password: 'B9-Lxqc2qJCB',
|
||||
description: 'Chave / senha de agendamento e autenticação do Chatwoot.',
|
||||
tags: ['Chatwoot', 'Agendamento', 'Senha']
|
||||
},
|
||||
{
|
||||
id: '23',
|
||||
title: 'Link do App Cobrador',
|
||||
category: 'financeiro',
|
||||
url: 'https://cobranca.sigefweb.com.br/login',
|
||||
description: 'Aplicativo de campo e login para os cobradores externos.',
|
||||
tags: ['Cobrador', 'App', 'Cobrança']
|
||||
}
|
||||
];
|
||||
|
||||
export function getStoredPortalItems() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Erro ao carregar dados do portal do cliente:', e);
|
||||
}
|
||||
|
||||
savePortalItems(INITIAL_PORTAL_ITEMS);
|
||||
return INITIAL_PORTAL_ITEMS;
|
||||
}
|
||||
|
||||
export function savePortalItems(items) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(items, null, 2));
|
||||
} catch (e) {
|
||||
console.error('Erro ao salvar dados do portal do cliente:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera o texto exato formatado para colar na descrição do grupo de WhatsApp
|
||||
*/
|
||||
export function generateWhatsAppDescriptionText(items) {
|
||||
return items.map(item => {
|
||||
const lines = [];
|
||||
lines.push(`*${item.title}*`);
|
||||
if (item.url) lines.push(item.url);
|
||||
if (item.secondaryUrl) lines.push(item.secondaryUrl);
|
||||
if (item.ipHost) lines.push(`IP: ${item.ipHost}`);
|
||||
if (item.username) lines.push(`Login: ${item.username}`);
|
||||
if (item.password) lines.push(`Senha: ${item.password}`);
|
||||
if (item.wifiNetwork) lines.push(`Rede: ${item.wifiNetwork}`);
|
||||
if (item.wifiPassword) lines.push(`Senha: ${item.wifiPassword}`);
|
||||
return lines.join('\n');
|
||||
}).join('\n\n');
|
||||
}
|
||||
Reference in New Issue
Block a user