feat: add Customer Portal (Área do Cliente & Central de Acessos) with 23 preloaded items and WhatsApp export

This commit is contained in:
2026-10-15 12:37:25 -03:00
parent 512cb4ea7e
commit d16ff4f70e
7 changed files with 1551 additions and 31 deletions
+21 -11
View File
@@ -4,10 +4,11 @@ import {
ArrowRight, ArrowRight,
RotateCcw, RotateCcw,
AlertCircle, AlertCircle,
Sparkles, Sparkles,
Lock, Lock,
Loader2, Loader2,
MessageSquare MessageSquare,
Bookmark
} from 'lucide-react'; } from 'lucide-react';
import Header from './components/Header'; import Header from './components/Header';
import SecurityBadge from './components/SecurityBadge'; import SecurityBadge from './components/SecurityBadge';
@@ -18,10 +19,11 @@ import OutputFiles from './components/OutputFiles';
import OpenSslReference from './components/OpenSslReference'; import OpenSslReference from './components/OpenSslReference';
import HostGuideModal from './components/HostGuideModal'; import HostGuideModal from './components/HostGuideModal';
import AppWPP from './components/wpp/AppWPP'; import AppWPP from './components/wpp/AppWPP';
import CustomerPortal from './components/portal/CustomerPortal';
import { convertPfx } from './utils/certConverter'; import { convertPfx } from './utils/certConverter';
export default function App() { 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 [file, setFile] = useState(null);
const [arrayBuffer, setArrayBuffer] = useState(null); const [arrayBuffer, setArrayBuffer] = useState(null);
const [baseName, setBaseName] = useState('certificado'); const [baseName, setBaseName] = useState('certificado');
@@ -102,9 +104,15 @@ export default function App() {
onOpenHostingGuide={() => setIsHostingGuideOpen(true)} onOpenHostingGuide={() => setIsHostingGuideOpen(true)}
/> />
{/* Conteúdo Principal */} {/* Conteúdo Principal Dinâmico */}
<main className="main-content"> <main className="main-content">
{activeTab === 'cert' ? ( {activeTab === 'portal' && (
/* Área do Cliente & Central de Acessos */
<CustomerPortal />
)}
{activeTab === 'cert' && (
/* Conversor de Certificados PFX */
<div className="container"> <div className="container">
{/* Banner de Segurança & Privacidade */} {/* Banner de Segurança & Privacidade */}
<SecurityBadge /> <SecurityBadge />
@@ -201,7 +209,9 @@ export default function App() {
</section> </section>
)} )}
</div> </div>
) : ( )}
{activeTab === 'wpp' && (
/* Aplicativo WPPConnect Integrado */ /* Aplicativo WPPConnect Integrado */
<div className="wpp-view-wrapper"> <div className="wpp-view-wrapper">
<AppWPP /> <AppWPP />
@@ -213,9 +223,9 @@ export default function App() {
<footer className="footer"> <footer className="footer">
<div className="footer-content"> <div className="footer-content">
<p> <p>
{activeTab === 'cert' {activeTab === 'portal' && 'InflavioApps — Área do Cliente & Central de Acessos C2 Sistemas.'}
? 'CertConverter Web — Conversão PKCS#12 / X.509 em conformidade com padrões ICP-Brasil e OpenSSL.' {activeTab === 'cert' && 'InflavioApps — 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 === 'wpp' && 'InflavioApps — Gerenciador de Sessões WPPConnect integrado ao portal.'}
</p> </p>
<div className="footer-links"> <div className="footer-links">
<button <button
+54 -20
View File
@@ -1,38 +1,62 @@
import React from 'react'; 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 }) { 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 ( return (
<header className="header"> <header className="header">
<div className="header-container"> <div className="header-container">
<div className="header-brand"> <div className="header-brand">
<div className="brand-icon-wrapper"> <div className={`brand-icon-wrapper ${current.iconClass}`}>
{activeTab === 'cert' ? ( <Icon className="brand-icon" size={24} />
<ShieldCheck className="brand-icon" size={26} />
) : (
<MessageSquare className="brand-icon" size={26} />
)}
</div> </div>
<div> <div>
<h1 className="brand-title"> <div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
{activeTab === 'cert' ? 'CertConverter PFX' : 'AppWPP Manager'} <span className="brand-prefix">InflavioApps</span>
</h1> <span className="brand-sep">/</span>
<p className="brand-subtitle"> <h1 className="brand-title">{current.title}</h1>
{activeTab === 'cert' </div>
? 'Conversor de Certificado Digital (.pfx para .crt, .pem, .key)' <p className="brand-subtitle">{current.sub}</p>
: 'Gerenciador de Sessões WPPConnect / WhatsApp'}
</p>
</div> </div>
</div> </div>
{/* Seletor Central de Aplicação */} {/* Seletor Central de Aplicações */}
<div className="app-nav-tabs"> <div className="app-nav-tabs">
<button <button
type="button" type="button"
className={`app-nav-btn ${activeTab === 'cert' ? 'app-nav-btn-active' : ''}`} className={`app-nav-btn ${activeTab === 'cert' ? 'app-nav-btn-active' : ''}`}
onClick={() => onSelectTab('cert')} onClick={() => onSelectTab('cert')}
> >
<ShieldCheck size={18} /> <ShieldCheck size={16} />
<span>Conversor PFX</span> <span>Conversor PFX</span>
</button> </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' : ''}`} className={`app-nav-btn ${activeTab === 'wpp' ? 'app-nav-btn-active app-nav-btn-wpp' : ''}`}
onClick={() => onSelectTab('wpp')} onClick={() => onSelectTab('wpp')}
> >
<MessageSquare size={18} /> <MessageSquare size={16} />
<span>AppWPP (WhatsApp)</span> <span>AppWPP</span>
<span className="nav-badge-pill">8</span> <span className="nav-badge-pill">8</span>
</button> </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>
<div className="header-actions"> <div className="header-actions">
@@ -54,7 +88,7 @@ export default function Header({ activeTab, onSelectTab, onOpenHostingGuide }) {
onClick={onOpenHostingGuide} onClick={onOpenHostingGuide}
title="Ver instruções de hospedagem da aplicação" title="Ver instruções de hospedagem da aplicação"
> >
<Server size={18} /> <Server size={17} />
<span>Hospedagem</span> <span>Hospedagem</span>
</button> </button>
</div> </div>
+358
View File
@@ -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>
);
}
+219
View File
@@ -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>
);
}
+174
View File
@@ -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
View File
@@ -2147,4 +2147,474 @@ body {
transform: translateY(-1px); 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;
}
+255
View File
@@ -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');
}