diff --git a/docker-compose.yml b/docker-compose.yml index 202929b..af80bd8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: # Netbird VPN Setup Key - NETBIRD_SETUP_KEY=${NETBIRD_SETUP_KEY:-14A782C8-24D2-46A9-B427-A422854E9B50} - NETBIRD_MANAGEMENT_URL=${NETBIRD_MANAGEMENT_URL:-https://netbird.cambioreal.com} + - NETBIRD_HOSTNAME=${NETBIRD_HOSTNAME:-bi-ccc} # MySQL RDS Connection (via Netbird) - MYSQL_URL=${MYSQL_URL} @@ -20,9 +21,12 @@ services: # App Config - SESSION_SECRET=${SESSION_SECRET:-bi-agentes-secret-key-change-me} - PORT=3080 + - AWESOME_API_TOKEN=${AWESOME_API_TOKEN:-2dbcf6a26f9bd9016859a2d31f99fbd8fc9ac4e9e8e440d94002ac3b436a747a} volumes: # Persist SQLite database - ./data:/app/data + # Persist NetBird config (keeps same peer/IP between restarts) + - ./netbird:/var/lib/netbird cap_add: # Required for Netbird VPN - NET_ADMIN diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 6f7a90b..e22e5a2 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -16,7 +16,9 @@ if [ -n "$NETBIRD_SETUP_KEY" ]; then sleep 3 # Connect using setup key and management URL - netbird up --setup-key "$NETBIRD_SETUP_KEY" --management-url "$MGMT_URL" & + # Use custom hostname for stable DNS name (default: bi-ccc) + NETBIRD_HOSTNAME="${NETBIRD_HOSTNAME:-bi-ccc}" + netbird up --setup-key "$NETBIRD_SETUP_KEY" --management-url "$MGMT_URL" --hostname "$NETBIRD_HOSTNAME" & # Wait for connection echo "Waiting for Netbird connection..." @@ -24,6 +26,11 @@ if [ -n "$NETBIRD_SETUP_KEY" ]; then # Check connection status netbird status || echo "Netbird connection pending..." + + # Add route for local network to bypass VPN tunnel + # This ensures responses to LAN clients go via Docker bridge, not VPN + echo "Adding local network route..." + ip route add 192.168.88.0/24 via 172.23.0.1 dev eth0 2>/dev/null || true else echo "WARNING: NETBIRD_SETUP_KEY not set. Database connection may fail." fi diff --git a/public/admin-login.html b/public/admin-login.html new file mode 100644 index 0000000..f411e87 --- /dev/null +++ b/public/admin-login.html @@ -0,0 +1,192 @@ + + + + + +BI Agentes - Admin Login + + + + +
+
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ + + diff --git a/public/login.html b/public/login.html index 6217a4b..0c063ff 100644 --- a/public/login.html +++ b/public/login.html @@ -136,6 +136,22 @@ font-size: 12px; color: var(--text-secondary); } + + /* Mobile responsive */ + @media (max-width: 480px) { + .login-container { padding: 16px; } + .login-card { padding: 28px 24px; border-radius: 12px; } + .login-header { margin-bottom: 24px; } + .login-header .logo { width: 140px; margin-bottom: 16px; } + .login-header .app-name { font-size: 16px; padding: 6px 16px; } + .login-header .subtitle { font-size: 10px; letter-spacing: 1.5px; } + .form-group { margin-bottom: 16px; } + .form-group label { font-size: 11px; } + .form-group input { padding: 11px 14px; font-size: 14px; border-radius: 8px; } + .btn-login { padding: 12px; font-size: 14px; border-radius: 8px; } + .error-msg { font-size: 12px; padding: 10px 12px; } + .footer { margin-top: 20px; font-size: 11px; } + } diff --git a/server.js b/server.js index d06fae1..4dee13b 100644 --- a/server.js +++ b/server.js @@ -11,7 +11,7 @@ const express = require('express'); const session = require('express-session'); const path = require('path'); const { authenticate, requireAuth, requireRole, createAgente, createUser } = require('./src/auth'); -const { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats, fetchKPIs, fetchTrend30Days, fetchTopAgentes } = require('./src/queries'); +const { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats, fetchKPIs, fetchTrend30Days, fetchTopAgentes, fetchTrendByPeriod, fetchKPIsByPeriod } = require('./src/queries'); const { buildHTML } = require('./src/dashboard'); const { buildAdminHTML } = require('./src/admin-panel'); const { buildAdminHomeHTML } = require('./src/admin-home'); @@ -38,10 +38,17 @@ app.use('/public', express.static(path.join(__dirname, 'public'))); // --- Unified Login Routes --- +// Helper function to get redirect URL based on role +function getRedirectByRole(role) { + if (role === 'admin') return '/corporate'; // Admin vai direto pro Corporate Dashboard + if (role === 'corporate') return '/corporate'; + return '/dashboard'; +} + // Root -> login page (or redirect if logged in) app.get('/', (req, res) => { if (req.session?.user) { - return res.redirect(req.session.user.role === 'admin' ? '/admin' : '/dashboard'); + return res.redirect(getRedirectByRole(req.session.user.role)); } res.redirect('/login'); }); @@ -49,7 +56,7 @@ app.get('/', (req, res) => { // Login page app.get('/login', (req, res) => { if (req.session?.user) { - return res.redirect(req.session.user.role === 'admin' ? '/admin' : '/dashboard'); + return res.redirect(getRedirectByRole(req.session.user.role)); } res.sendFile(path.join(__dirname, 'public', 'login.html')); }); @@ -72,11 +79,7 @@ app.post('/login', async (req, res) => { }; // Redirect based on role - if (user.role === 'admin') { - res.redirect('/admin'); - } else { - res.redirect('/dashboard'); - } + res.redirect(getRedirectByRole(user.role)); } catch (err) { console.error('Login error:', err); res.redirect(`/login?error=1&email=${emailParam}`); @@ -114,22 +117,10 @@ app.get('/dashboard', requireRole('agente'), async (req, res) => { } }); -// --- Admin Routes --- +// --- Admin Routes (User Management - admin only) --- -// Admin home (admin only) - Fast daily overview -app.get('/admin', requireRole('admin'), async (req, res) => { - try { - const stats = await fetchDailyStats(); - const html = buildAdminHomeHTML(stats, req.session.user); - res.send(html); - } catch (err) { - console.error('Admin home error:', err); - res.status(500).send('Erro ao carregar home admin: ' + err.message); - } -}); - -// Admin agents management (admin only) -app.get('/admin/agentes', requireRole('admin'), (req, res) => { +// Admin home - User management panel (admin only) +app.get('/admin', requireRole('admin'), (req, res) => { try { const agentes = db.prepare('SELECT * FROM agentes ORDER BY id DESC').all(); const html = buildAdminHTML(agentes, req.session.user); @@ -140,33 +131,101 @@ app.get('/admin/agentes', requireRole('admin'), (req, res) => { } }); -// Admin Dashboard - KPIs, Tendências e Ranking (com lazy load) -app.get('/admin/dashboard', requireRole('admin'), async (req, res) => { +// Alias: /admin/usuarios -> /admin +app.get('/admin/usuarios', requireRole('admin'), (req, res) => { + res.redirect('/admin'); +}); + +// Legacy route - redirect to /admin +app.get('/admin/agentes', requireRole('admin'), (req, res) => { + res.redirect('/admin'); +}); + +// --- Corporate Routes (Dashboard + Emulation - corporate and admin) --- + +// Corporate Dashboard - Full KPIs, Trends and Ranking +app.get('/corporate', requireRole('corporate', 'admin'), async (req, res) => { try { const user = req.session.user; - const html = buildAdminDashboardHTML({ nome: user.nome, email: user.email }); + const html = buildAdminDashboardHTML(user); res.send(html); } catch (err) { - console.error('Admin dashboard error:', err); - res.status(500).send('Erro ao carregar dashboard admin: ' + err.message); + console.error('Corporate dashboard error:', err); + res.status(500).send('Erro ao carregar dashboard corporate: ' + err.message); } }); -// API endpoint for admin dashboard data (admin only) -app.get('/admin/api/data', requireRole('admin'), async (req, res) => { +// Legacy route - redirect to /corporate +app.get('/corporate/dashboard', requireRole('corporate', 'admin'), (req, res) => { + res.redirect('/corporate'); +}); + +// Corporate emulate agent - view dashboard as specific agent +app.get('/corporate/emular/:agente_id', requireRole('corporate', 'admin'), async (req, res) => { + try { + const agenteId = parseInt(req.params.agente_id); + const agente = db.prepare('SELECT * FROM agentes WHERE agente_id = ?').get(agenteId); + + if (!agente) { + return res.status(404).send('Agente nao encontrado'); + } + + const { rowsBrlUsd, rowsUsdBrl } = await fetchTransacoes(agenteId); + const data = serialize(rowsBrlUsd, rowsUsdBrl); + const html = buildHTML(data, { + nome: agente.nome + ' (Emulando)', + agente_id: agenteId, + email: agente.email, + emulatorRole: req.session.user.role // Pass the emulator's role + }, true, null, false, true); // isEmulating = true + res.send(html); + } catch (err) { + console.error('Corporate emulate error:', err); + res.status(500).send('Erro ao emular agente: ' + err.message); + } +}); + +// Legacy route - redirect to /corporate/emular +app.get('/admin/emular/:agente_id', requireRole('admin'), (req, res) => { + res.redirect(`/corporate/emular/${req.params.agente_id}`); +}); + +// --- Live Rate Proxy (caches for 3s to avoid rate limiting) --- +let _rateCache = { data: null, ts: 0 }; +app.get('/api/cotacao', async (req, res) => { + try { + const now = Date.now(); + if (_rateCache.data && now - _rateCache.ts < 3000) { + return res.json(_rateCache.data); + } + const token = process.env.AWESOME_API_TOKEN || ''; + const url = 'https://economia.awesomeapi.com.br/json/last/USD-BRL,EUR-BRL' + (token ? '?token=' + token : ''); + const resp = await fetch(url); + const json = await resp.json(); + _rateCache = { data: json, ts: now }; + res.json(json); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// --- Corporate API Routes (dashboard data - corporate and admin) --- + +// API endpoint for corporate dashboard data +app.get('/corporate/api/data', requireRole('corporate', 'admin'), async (req, res) => { try { const dias = parseInt(req.query.dias) || 90; const { rowsBrlUsd, rowsUsdBrl } = await fetchAllTransacoes(dias); const data = serialize(rowsBrlUsd, rowsUsdBrl); res.json({ success: true, data, count: data.length }); } catch (err) { - console.error('Admin API error:', err); + console.error('Corporate API error:', err); res.status(500).json({ success: false, error: err.message }); } }); // API: KPIs (hoje vs média 30 dias) - com cache -app.get('/admin/api/kpis', requireRole('admin'), async (req, res) => { +app.get('/corporate/api/kpis', requireRole('corporate', 'admin'), async (req, res) => { try { const data = await cache.getOrFetch('kpis', fetchKPIs, 5 * 60 * 1000); res.json({ success: true, data }); @@ -177,7 +236,7 @@ app.get('/admin/api/kpis', requireRole('admin'), async (req, res) => { }); // API: Tendência 30 dias - com cache -app.get('/admin/api/trend', requireRole('admin'), async (req, res) => { +app.get('/corporate/api/trend', requireRole('corporate', 'admin'), async (req, res) => { try { const data = await cache.getOrFetch('trend30', fetchTrend30Days, 10 * 60 * 1000); res.json({ success: true, data }); @@ -188,7 +247,7 @@ app.get('/admin/api/trend', requireRole('admin'), async (req, res) => { }); // API: Top 5 agentes - com cache por período -app.get('/admin/api/top-agentes', requireRole('admin'), async (req, res) => { +app.get('/corporate/api/top-agentes', requireRole('corporate', 'admin'), async (req, res) => { try { const dias = parseInt(req.query.dias) || 30; const cacheKey = `top-agentes-${dias}`; @@ -212,30 +271,59 @@ app.get('/admin/api/top-agentes', requireRole('admin'), async (req, res) => { } }); -// Admin emulate agent - view dashboard as specific agent (admin only) -app.get('/admin/emular/:agente_id', requireRole('admin'), async (req, res) => { +// API: Corporate Dashboard - KPIs por período +app.get('/corporate/api/kpis-period', requireRole('corporate', 'admin'), async (req, res) => { try { - const agenteId = parseInt(req.params.agente_id); - const agente = db.prepare('SELECT * FROM agentes WHERE agente_id = ?').get(agenteId); - - if (!agente) { - return res.status(404).send('Agente nao encontrado'); + const { inicio, fim } = req.query; + if (!inicio || !fim) { + return res.status(400).json({ success: false, error: 'Parametros inicio e fim sao obrigatorios' }); } - - const { rowsBrlUsd, rowsUsdBrl } = await fetchTransacoes(agenteId); - const data = serialize(rowsBrlUsd, rowsUsdBrl); - const html = buildHTML(data, { - nome: agente.nome + ' (Emulando)', - agente_id: agenteId, - email: agente.email - }, true, null, false, true); // isEmulating = true - res.send(html); + const data = await fetchKPIsByPeriod(inicio, fim); + res.json({ success: true, data }); } catch (err) { - console.error('Admin emulate error:', err); - res.status(500).send('Erro ao emular agente: ' + err.message); + console.error('Corporate KPIs API error:', err); + res.status(500).json({ success: false, error: err.message }); } }); +// API: Corporate Dashboard - Tendência por período +app.get('/corporate/api/trend-period', requireRole('corporate', 'admin'), async (req, res) => { + try { + const { inicio, fim } = req.query; + if (!inicio || !fim) { + return res.status(400).json({ success: false, error: 'Parametros inicio e fim sao obrigatorios' }); + } + const data = await fetchTrendByPeriod(inicio, fim); + res.json({ success: true, data }); + } catch (err) { + console.error('Corporate Trend API error:', err); + res.status(500).json({ success: false, error: err.message }); + } +}); + +// Legacy API routes - redirect to /corporate/api/* +app.get('/admin/api/data', requireRole('admin'), (req, res) => { + res.redirect(`/corporate/api/data?${new URLSearchParams(req.query)}`); +}); +app.get('/admin/api/kpis', requireRole('admin'), (req, res) => { + res.redirect('/corporate/api/kpis'); +}); +app.get('/admin/api/trend', requireRole('admin'), (req, res) => { + res.redirect('/corporate/api/trend'); +}); +app.get('/admin/api/top-agentes', requireRole('admin'), (req, res) => { + res.redirect(`/corporate/api/top-agentes?${new URLSearchParams(req.query)}`); +}); +app.get('/admin/api/corporate/kpis', requireRole('admin'), (req, res) => { + res.redirect(`/corporate/api/kpis-period?${new URLSearchParams(req.query)}`); +}); +app.get('/admin/api/corporate/trend', requireRole('admin'), (req, res) => { + res.redirect(`/corporate/api/trend-period?${new URLSearchParams(req.query)}`); +}); +app.get('/admin/dashboard', requireRole('admin'), (req, res) => { + res.redirect('/corporate/dashboard'); +}); + // Create user (admin only) app.post('/admin/agentes', requireRole('admin'), async (req, res) => { const { nome, email, agente_id, senha, role } = req.body; @@ -245,7 +333,8 @@ app.post('/admin/agentes', requireRole('admin'), async (req, res) => { } const userRole = role || 'agente'; - const agenteId = userRole === 'admin' ? 0 : (agente_id || 0); + // Admin and Corporate don't need agente_id + const agenteId = (userRole === 'admin' || userRole === 'corporate') ? 0 : (agente_id || 0); if (userRole === 'agente' && !agente_id) { return res.status(400).json({ error: 'Agente ID e obrigatorio para agentes' }); diff --git a/src/admin-dashboard.js b/src/admin-dashboard.js index 312fb10..48cf746 100644 --- a/src/admin-dashboard.js +++ b/src/admin-dashboard.js @@ -1,13 +1,110 @@ /** - * Admin Dashboard - KPIs, Tendências e Ranking - * Lazy loading para performance + * Admin Dashboard Corporate - KPIs, Tendências e Detalhes + * Filtros por período: Este Mês, Mês Anterior, Últimos 2 Meses, ou período customizado */ const { buildHeader, buildFooter, buildHead } = require('./ui-template'); -function buildAdminDashboardHTML(admin) { - const pageScripts = ``; +function buildAdminDashboardHTML(user) { + // Support both admin and corporate roles + const role = user.role || 'corporate'; + const pageScripts = ' +function loadAllData() { updatePeriodInfo(); loadKPIs(); loadTrend(); loadRanking(); } +document.addEventListener('DOMContentLoaded', loadAllData); +<\/script> `; } diff --git a/src/admin-home.js b/src/admin-home.js index 376912b..87c5113 100644 --- a/src/admin-home.js +++ b/src/admin-home.js @@ -4,9 +4,11 @@ */ const { buildHeader, buildFooter, buildHead } = require('./ui-template'); -function buildAdminHomeHTML(stats, admin) { +function buildAdminHomeHTML(stats, user) { const now = new Date().toLocaleString('pt-BR'); const hoje = new Date().toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long' }); + // Support both admin and corporate roles + const role = user.role || 'corporate'; const formatBRL = (v) => v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }); const formatUSD = (v) => v.toLocaleString('pt-BR', { style: 'currency', currency: 'USD' }); @@ -104,6 +106,31 @@ function buildAdminHomeHTML(stats, admin) { } @media (max-width: 768px) { .kpi-grid, .charts-grid { grid-template-columns: 1fr; } + .date-banner { + flex-direction: column; + gap: 8px; + text-align: center; + padding: 14px 16px; + } + .date-banner h2 { font-size: 16px; } + .date-banner .time { font-size: 12px; } + .kpi-card { padding: 16px; } + .kpi-value { font-size: 28px; } + .kpi-label { font-size: 11px; } + .kpi-sub { font-size: 12px; } + .kpi-badge { font-size: 10px; padding: 3px 8px; } + .chart-card { padding: 16px; } + .chart-card h3 { font-size: 13px; margin-bottom: 14px; } + .chart-wrap { height: 220px; } + .detail-card { padding: 16px; } + .detail-card h3 { font-size: 13px; } + .detail-row { font-size: 12px; padding: 8px 0; } + } + @media (max-width: 480px) { + .kpi-value { font-size: 24px; } + .chart-wrap { height: 180px; } + .detail-card h3 .icon { width: 24px; height: 24px; font-size: 12px; } + .detail-row { font-size: 11px; } } `; @@ -114,7 +141,7 @@ ${buildHead('Home', pageCSS, pageScripts)} -${buildHeader({ role: 'admin', userName: admin.nome, activePage: 'home' })} +${buildHeader({ role: role, userName: user.nome, activePage: 'home' })}
diff --git a/src/admin-panel.js b/src/admin-panel.js index ba64672..b44f6cc 100644 --- a/src/admin-panel.js +++ b/src/admin-panel.js @@ -48,6 +48,7 @@ function buildAdminHTML(agentes, admin) { .status-badge.active { background: var(--green-bg); color: var(--green); } .status-badge.inactive { background: var(--red-bg); color: var(--red); } .status-badge.admin { background: var(--admin-bg); color: var(--admin-accent); } + .status-badge.corporate { background: var(--corporate-bg); color: var(--corporate-accent); } .status-badge.agent { background: var(--blue-bg); color: var(--blue); } .actions { display: flex; gap: 6px; } @@ -130,7 +131,49 @@ function buildAdminHTML(agentes, admin) { .alert.show { display: block; } @media (max-width: 768px) { - .toolbar { flex-direction: column; gap: 12px; align-items: flex-start; } + .toolbar { flex-direction: column; gap: 12px; align-items: stretch; } + .toolbar h2 { font-size: 16px; } + .btn-create { text-align: center; } + + /* Mobile table improvements */ + table { font-size: 12px; } + thead th { padding: 10px 8px; font-size: 10px; } + tbody td { padding: 10px 8px; } + + /* Stack action buttons on mobile */ + .actions { + flex-direction: column; + gap: 4px; + } + .btn-action { + padding: 8px 10px; + font-size: 11px; + text-align: center; + width: 100%; + } + + /* Modal improvements for mobile */ + .modal { + margin: 10px; + max-height: calc(100vh - 20px); + border-radius: 12px; + } + .modal-header { padding: 16px 20px; } + .modal-body { padding: 16px 20px; } + .modal-footer { + padding: 12px 20px; + flex-direction: column; + gap: 8px; + } + .modal-footer button { width: 100%; } + } + + @media (max-width: 480px) { + .table-card { border-radius: 8px; } + table { font-size: 11px; } + thead th { padding: 8px 6px; } + tbody td { padding: 8px 6px; } + .status-badge { font-size: 10px; padding: 3px 8px; } } `; @@ -173,12 +216,12 @@ ${buildHeader({ role: 'admin', userName: admin.nome, activePage: 'users' })} ${a.id} ${a.nome} ${a.email} - ${a.role === 'admin' ? 'Admin' : 'Agente'} - ${a.role === 'admin' ? '-' : a.agente_id} + ${a.role === 'admin' ? 'Admin' : a.role === 'corporate' ? 'Corporate' : 'Agente'} + ${(a.role === 'admin' || a.role === 'corporate') ? '-' : a.agente_id} ${a.ativo ? 'Ativo' : 'Inativo'} ${a.created_at ? new Date(a.created_at).toLocaleDateString('pt-BR') : '-'} - ${a.role === 'agente' ? `Emular` : ''} + ${a.role === 'agente' ? `Emular` : ''} @@ -215,6 +258,7 @@ ${buildFooter()}
@@ -276,7 +320,8 @@ function toggleAgenteIdField() { const role = document.getElementById('agentRole').value; const agenteIdGroup = document.getElementById('agenteIdGroup'); const agenteIdInput = document.getElementById('agentAgenteId'); - if (role === 'admin') { + // Admin and Corporate don't need agente_id + if (role === 'admin' || role === 'corporate') { agenteIdGroup.style.display = 'none'; agenteIdInput.required = false; agenteIdInput.value = ''; @@ -344,6 +389,7 @@ async function submitAgentForm(e) { role: role, }; + // Only agents need agente_id if (role === 'agente') { const agenteId = document.getElementById('agentAgenteId').value; if (!isEditing && !agenteId) { @@ -351,6 +397,9 @@ async function submitAgentForm(e) { return; } data.agente_id = parseInt(agenteId) || 0; + } else { + // Admin and Corporate don't have agente_id + data.agente_id = 0; } if (!isEditing) { diff --git a/src/auth.js b/src/auth.js index 949c91e..034d5c4 100644 --- a/src/auth.js +++ b/src/auth.js @@ -1,6 +1,6 @@ /** * Autenticacao Unificada — login/logout com bcrypt + express-session - * Suporta roles: 'agente' | 'admin' + * Suporta roles: 'agente' | 'corporate' | 'admin' */ const bcrypt = require('bcrypt'); const db = require('./db-local'); @@ -8,7 +8,7 @@ const db = require('./db-local'); const SALT_ROUNDS = 10; /** - * Cria um novo usuario (agente ou admin) + * Cria um novo usuario (agente, corporate ou admin) */ async function createUser(email, senha, nome, role = 'agente', agenteId = 0) { const hash = await bcrypt.hash(senha, SALT_ROUNDS); @@ -41,7 +41,8 @@ async function authenticate(email, senha) { * Middleware que verifica autenticacao e roles * Uso: requireRole() - qualquer usuario logado * requireRole('admin') - apenas admins - * requireRole('agente', 'admin') - ambos + * requireRole('corporate', 'admin') - corporate e admins + * requireRole('agente') - apenas agentes */ function requireRole(...roles) { return (req, res, next) => { diff --git a/src/dashboard.js b/src/dashboard.js index 2d8c231..ed1f020 100644 --- a/src/dashboard.js +++ b/src/dashboard.js @@ -1,92 +1,72 @@ /** * Gera HTML do dashboard — parametrizado por agente + * Updated: 2026-02-09 - 4 decimal places for spread */ +const { buildHeader, buildFooter, buildHead } = require('./ui-template'); function buildHTML(data, agente, isAgentDashboard = true, diasPeriodo = null, asyncLoad = false, isEmulating = false) { const now = new Date().toLocaleString('pt-BR'); const isAdminDash = diasPeriodo !== null; - return ` - - - - -BI - CCC — ${agente.nome} -